Test Setup Failed
Push — master ( a5724a...76324d )
by Stefan
07:15
created

InputValidation::existingIdP()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 12
c 1
b 0
f 0
dl 0
loc 21
rs 8.8333
cc 7
nc 6
nop 3
1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
namespace web\lib\common;
24
25
use \Exception;
26
27
/**
28
 * performs validation of user inputs
29
 */
30
class InputValidation extends \core\common\Entity
31
{
32
33
    /**
34
     * returns a simple HTML <p> element with basic explanations about what was
35
     * wrong with the input
36
     * 
37
     * @param string $customtext explanation provided by the validator function
38
     * @return string
39
     */
40
    private function inputValidationError($customtext)
41
    {
42
        \core\common\Entity::intoThePotatoes();
43
        $retval = "<p>" . _("Input validation error: ") . $customtext . "</p>";
44
        \core\common\Entity::outOfThePotatoes();
45
        return $retval;
46
    }
47
48
    /**
49
     * Is this a known Federation? Optionally, also check if the authenticated
50
     * user is a federation admin of that federation
51
     * @param mixed       $input the ISO code of the federation
52
     * @param string|NULL $owner the authenticated username, optional
53
     * @return \core\Federation
54
     * @throws Exception
55
     */
56
    public function existingFederation($input, $owner = NULL)
57
    {
58
59
        $cat = new \core\CAT(); // initialises Entity static members
60
        $fedIdentifiers = array_keys($cat->knownFederations);
61
        if (!in_array(strtoupper($input), $fedIdentifiers)) {
62
            throw new Exception($this->inputValidationError(sprintf("This %s does not exist!", \core\common\Entity::$nomenclature_fed)));
63
        }
64
        // totally circular, but this hopefully *finally* make Scrutinizer happier
65
        $correctIndex = array_search(strtoupper($input), $fedIdentifiers);
66
        $postFed = $fedIdentifiers[$correctIndex];
67
68
        $temp = new \core\Federation($postFed);
69
        if ($owner === NULL) {
70
            return $temp;
71
        }
72
73
        foreach ($temp->listFederationAdmins() as $oneowner) {
74
            if ($oneowner == $owner) {
75
                return $temp;
76
            }
77
        }
78
        throw new Exception($this->inputValidationError(sprintf("User is not %s administrator!", \core\common\Entity::$nomenclature_fed)));
79
    }
80
81
    /**
82
     * Is this a known IdP? Optionally, also check if the authenticated
83
     * user is an admin of that IdP
84
     * @param mixed            $input             the numeric ID of the IdP in the system
85
     * @param string           $owner             the authenticated username, optional
86
     * @param \core\Federation $claimedFedBinding if set, cross-check that IdP belongs to specified federation (useful in admin API mode)
87
     * @return \core\IdP
88
     * @throws Exception
89
     */
90
    public function existingIdP($input, $owner = NULL, $claimedFedBinding = NULL)
91
    {
92
        $clean = $this->integer($input);
93
        if ($clean === FALSE) {
94
            throw new Exception($this->inputValidationError("Value for IdP is not an integer!"));
95
        }
96
97
        $temp = new \core\IdP($input); // constructor throws an exception if NX, game over
98
99
        if ($owner !== NULL) { // check if the authenticated user is allowed to see this institution
100
            foreach ($temp->listOwners() as $oneowner) {
101
                if ($oneowner['ID'] == $owner) {
102
                    return $temp;
103
                }
104
            }
105
            throw new Exception($this->inputValidationError("This IdP identifier is not accessible!"));
106
        }
107
        if ($claimedFedBinding !== NULL && strtoupper($temp->federation) != strtoupper($claimedFedBinding->tld)) {
108
            throw new Exception($this->inputValidationError("This IdP does not belong to the claimed federation!"));
109
        }
110
        return $temp;
111
    }
112
113
    /**
114
     * Checks if the input refers to a known Profile. Optionally also takes an
115
     * IdP identifier and then checks if the Profile belongs to the refernced 
116
     * IdP
117
     * 
118
     * @param mixed    $input         the numeric ID of the Profile in the system
119
     * @param int|NULL $idpIdentifier the numeric ID of the IdP in the system, optional
120
     * @return \core\AbstractProfile
121
     * @throws Exception
122
     */
123
    public function existingProfile($input, $idpIdentifier = NULL)
124
    {
125
        $clean = $this->integer($input);
126
        if ($clean === FALSE) {
127
            throw new Exception("Non-integer was passed to Profile validator!");
128
        }
129
        $temp = \core\ProfileFactory::instantiate($clean); // constructor throws an exception if NX, game over
130
131
        if ($idpIdentifier !== NULL && $temp->institution != $idpIdentifier) {
132
            throw new Exception($this->inputValidationError("The profile does not belong to the IdP!"));
133
        }
134
        return $temp;
135
    }
136
137
    /**
138
     * Checks if the input refers to a known DeploymentManaged. Optionally also takes an
139
     * IdP identifier and then checks if the Profile belongs to the refernced 
140
     * IdP
141
     * 
142
     * @param mixed     $input the numeric ID of the Deployment in the system
143
     * @param \core\IdP $idp   the IdP
144
     * @return \core\DeploymentManaged
145
     * @throws Exception
146
     */
147
    public function existingDeploymentManaged($input, $idp)
148
    {
149
        $clean = $this->integer($input);
150
        if ($clean === FALSE) {
151
            throw new Exception("Non-integer was passed to Profile validator!");
152
        }
153
        $temp = new \core\DeploymentManaged($idp, $clean); // constructor throws an exception if NX, game over
154
155
        if ($temp->institution != $idp->identifier) {
156
            throw new Exception($this->inputValidationError("The profile does not belong to the IdP!"));
157
        }
158
        return $temp;
159
    }
160
161
    /**
162
     * Checks if this is a device known to the system
163
     * @param mixed $input the name of the device (index in the Devices.php array)
164
     * @return string returns the same string on success, throws an Exception on failure
165
     * @throws Exception
166
     */
167
    public function existingDevice($input)
168
    {
169
        $devicelist = \devices\Devices::listDevices();
170
        $keyArray = array_keys($devicelist);
171
        if (!isset($devicelist[$input])) {
172
            throw new Exception($this->inputValidationError("This device does not exist!"));
173
        }
174
        $correctIndex = array_search($input, $keyArray);
175
        return $keyArray[$correctIndex];
176
    }
177
178
    /**
179
     * Checks if the input was a valid string.
180
     * 
181
     * @param mixed   $input           a string to be made SQL-safe
182
     * @param boolean $allowWhitespace whether some whitespace (e.g. newlines should be preserved (true) or redacted (false)
183
     * @return string the massaged string
184
     * @throws Exception
185
     */
186
    public function string($input, $allowWhitespace = FALSE)
187
    {
188
        // always chop out invalid characters, and surrounding whitespace
189
        $retvalStep0 = iconv("UTF-8", "UTF-8//TRANSLIT", $input);
190
        if ($retvalStep0 === FALSE) {
191
            throw new Exception("iconv failure for string sanitisation. With TRANSLIT, this should never happen!");
192
        }
193
        $retvalStep1 = trim($retvalStep0);
194
        // if some funny person wants to inject markup tags, remove them
195
        $retval = filter_var($retvalStep1, FILTER_SANITIZE_STRING, ["flags" => FILTER_FLAG_NO_ENCODE_QUOTES]);
196
        if ($retval === FALSE) {
197
            throw new Exception("filter_var failure for string sanitisation.");
198
        }
199
        // unless explicitly wanted, take away intermediate disturbing whitespace
200
        // a simple "space" is NOT disturbing :-)
201
        if ($allowWhitespace === FALSE) {
202
            $afterWhitespace = preg_replace('/(\0|\r|\x0b|\t|\n)/', '', $retval);
203
        } else {
204
            // even if we allow whitespace, not pathological ones!
205
            $afterWhitespace = preg_replace('/(\0|\r|\x0b)/', '', $retval);
206
        }
207
        if (is_array($afterWhitespace)) {
208
            throw new Exception("This function has to be given a string and returns a string. preg_replace has generated an array instead!");
209
        }
210
        return (string) $afterWhitespace;
211
    }
212
213
    /**
214
     * Is this an integer, or a string that represents an integer?
215
     * 
216
     * @param mixed $input the raw input
217
     * @return boolean|int returns the input, or FALSE if it is not an integer-like value
218
     */
219
    public function integer($input)
220
    {
221
        if (is_numeric($input)) {
222
            return (int) $input;
223
        }
224
        return FALSE;
225
    }
226
227
    /**
228
     * Is this a string representing a potentially more than 64-Bit length integer?
229
     * 
230
     * @param string $input the input data which is possibly a really large integer
231
     * @return boolean|string returns the input, or FALSE if it is not an integer-like string
232
     */
233
    public function hugeInteger($input)
234
    {
235
        if (is_numeric($input)) {
236
            return $input;
237
        }
238
        return FALSE;
239
    }
240
241
    /**
242
     * Checks if the input is the hex representation of a Consortium OI (i.e. three
243
     * or five bytes)
244
     * 
245
     * @param mixed $input the raw input
246
     * @return boolean|string returns the input, or FALSE on validation failure
247
     */
248
    public function consortiumOI($input)
249
    {
250
        $shallow = $this->string($input);
251
        if (strlen($shallow) != 6 && strlen($shallow) != 10) {
252
            return FALSE;
253
        }
254
        if (!preg_match("/^[a-fA-F0-9]+$/", $shallow)) {
255
            return FALSE;
256
        }
257
        return $shallow;
258
    }
259
260
    /**
261
     * Is the input an NAI realm? Throws HTML error and returns FALSE if not.
262
     * 
263
     * @param mixed $input the input to check
264
     * @return boolean|string returns the realm, or FALSE if it was malformed
265
     */
266
    public function realm($input)
267
    {
268
        \core\common\Entity::intoThePotatoes();
269
        if (strlen($input) == 0) {
270
            echo $this->inputValidationError(_("Realm is empty!"));
271
            \core\common\Entity::outOfThePotatoes();
272
            return FALSE;
273
        }
274
275
        // basic string checks
276
        $check = $this->string($input);
277
        // list of things to check, and the error they produce
278
        $pregCheck = [
279
            "/@/" => _("Realm contains an @ sign!"),
280
            "/^\./" => _("Realm begins with a . (dot)!"),
281
            "/\.$/" => _("Realm ends with a . (dot)!"),
282
            "/ /" => _("Realm contains spaces!"),
283
        ];
284
285
        // bark on invalid constructs
286
        foreach ($pregCheck as $search => $error) {
287
            if (preg_match($search, $check) == 1) {
288
                echo $this->inputValidationError($error);
289
                \core\common\Entity::outOfThePotatoes();
290
                return FALSE;
291
            }
292
        }
293
294
        if (preg_match("/\./", $check) == 0) {
295
            echo $this->inputValidationError(_("Realm does not contain at least one . (dot)!"));
296
            \core\common\Entity::outOfThePotatoes();
297
            return FALSE;
298
        }
299
300
        // none of the special HTML entities should be here. In case someone wants
301
        // to mount a CSS attack by providing something that matches the realm constructs
302
        // below but has interesting stuff between, mangle the input so that these
303
        // characters do not do any harm.
304
        \core\common\Entity::outOfThePotatoes();
305
        return htmlentities($check, ENT_QUOTES);
306
    }
307
308
    /**
309
     * could this be a valid username? 
310
     * 
311
     * Only checks correct form, not if the user actually exists in the system.
312
     * 
313
     * @param mixed $input the username
314
     * @return string echoes back the input string, or throws an Exception if bogus
315
     * @throws Exception
316
     */
317
    public function syntaxConformUser($input)
318
    {
319
        $retvalStep0 = iconv("UTF-8", "UTF-8//TRANSLIT", $input);
320
        if ($retvalStep0 === FALSE) {
321
            throw new Exception("iconv failure for string sanitisation. With TRANSLIT, this should never happen!");
322
        }
323
        $retvalStep1 = trim($retvalStep0);
324
325
        $retval = preg_replace('/(\0|\r|\x0b|\t|\n)/', '', $retvalStep1);
326
        if ($retval != "" && !ctype_print($retval)) {
327
            throw new Exception($this->inputValidationError("The user identifier is not an ASCII string!"));
328
        }
329
330
        return $retval;
331
    }
332
333
    /**
334
     * could this be a valid token? 
335
     * 
336
     * Only checks correct form, not if the token actually exists in the system.
337
     * @param mixed $input the raw input
338
     * @return string echoes back the input string, or throws an Exception if bogus
339
     * @throws Exception
340
     */
341
    public function token($input)
342
    {
343
        $retval = $input;
344
        if ($input != "" && preg_match('/[^0-9a-fA-F]/', $input) != 0) {
345
            throw new Exception($this->inputValidationError("Token is not a hexadecimal string!"));
346
        }
347
        return $retval;
348
    }
349
350
    /**
351
     * Is this be a valid coordinate vector on one axis?
352
     * 
353
     * @param mixed $input a numeric value in range of a geo coordinate [-180;180]
354
     * @return string returns back the input if all is good; throws an Exception if out of bounds or not numeric
355
     * @throws Exception
356
     */
357
    public function coordinate($input)
358
    {
359
        $oldlocale = setlocale(LC_NUMERIC, 0);
360
        setlocale(LC_NUMERIC, "en_GB");
361
        if (!is_numeric($input)) {
362
            throw new Exception($this->inputValidationError("Coordinate is not a numeric value!"));
363
        }
364
        setlocale(LC_NUMERIC, $oldlocale);
365
        // lat and lon are always in the range of [-180;+180]
366
        if ($input < -180 || $input > 180) {
367
            throw new Exception($this->inputValidationError("Coordinate is out of bounds. Which planet are you from?"));
368
        }
369
        return $input;
370
    }
371
372
    /**
373
     * Is this a valid coordinate pair in JSON encoded representation?
374
     * 
375
     * @param mixed $input the string to be checked: is this a serialised array with lat/lon keys in a valid number range?
376
     * @return string returns $input if checks have passed; throws an Exception if something's wrong
377
     * @throws Exception
378
     */
379
    public function coordJsonEncoded($input)
380
    {
381
        $tentative = json_decode($input, true);
382
        if (is_array($tentative)) {
383
            if (isset($tentative['lon']) && isset($tentative['lat']) && $this->coordinate($tentative['lon']) && $this->coordinate($tentative['lat'])) {
384
                return $input;
385
            }
386
        }
387
        throw new Exception($this->inputValidationError("Wrong coordinate encoding (2.0 uses JSON, not serialize)!"));
388
    }
389
390
    /**
391
     * This checks the state of a HTML GET/POST "boolean".
392
     * 
393
     * If not checked, no value is submitted at all; if checked, has the word "on". 
394
     * Anything else is a big error.
395
     * 
396
     * @param mixed $input the string to test
397
     * @return boolean TRUE if the input was "on". It is not possible in HTML to signal "off"
398
     * @throws Exception
399
     */
400
    public function boolean($input)
401
    {
402
        if ($input != "on") {
403
            throw new Exception($this->inputValidationError("Unknown state of boolean option!"));
404
        }
405
        return TRUE;
406
    }
407
408
    /**
409
     * checks if we have the strings "IdP" "SP" or "IdPSP"
410
     * 
411
     * @param string $partTypeRaw the string to be validated as participant type
412
     * @return string validated result
413
     * @throws Exception
414
     */
415
    public function partType($partTypeRaw)
416
    {
417
        switch ($partTypeRaw) {
418
            case \core\IdP::TYPE_IDP:
419
                return \core\IdP::TYPE_IDP;
420
            case \core\IdP::TYPE_SP:
421
                return \core\IdP::TYPE_SP;
422
            case \core\IdP::TYPE_IDPSP:
423
                return \core\IdP::TYPE_IDPSP;
424
            default:
425
                throw new Exception("Unknown Participant Type!");
426
        }
427
    }
428
429
    const TABLEMAPPING = [
430
        "IdP" => "institution_option",
431
        "Profile" => "profile_option",
432
        "FED" => "federation_option",
433
    ];
434
435
    /**
436
     * Is this a valid database reference? Has the form <tablename>-<rowID> and there
437
     * needs to be actual data at that place
438
     * 
439
     * @param string $input the reference to check
440
     * @return boolean|array the reference split up into "table" and "rowindex", or FALSE
441
     */
442
    public function databaseReference($input)
443
    {
444
        $pregMatches = [];
445
        if (preg_match("/^ROWID-(IdP|Profile|FED)-([0-9]+)$/", $input, $pregMatches) != 1) {
446
            return FALSE;
447
        }
448
        $rownumber = $this->integer($pregMatches[2]);
449
        if ($rownumber === FALSE) {
450
            return FALSE;
451
        }
452
        return ["table" => self::TABLEMAPPING[$pregMatches[1]], "rowindex" => $rownumber];
453
    }
454
455
    /**
456
     * is this a valid hostname?
457
     * 
458
     * @param mixed $input the raw input
459
     * @return boolean|string echoes the hostname, or FALSE if bogus
460
     */
461
    public function hostname($input)
462
    {
463
        // is it a valid IP address (IPv4 or IPv6), or a hostname?
464
        if (filter_var($input, FILTER_VALIDATE_IP) || filter_var($input, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
465
            // if it's a verified IP address or hostname then it does not contain
466
            // rubbish of course. But just to be sure, run htmlspecialchars around it
467
            return htmlspecialchars($input, ENT_QUOTES);
468
        }
469
        return FALSE;
470
    }
471
472
    /**
473
     * is this a valid email address?
474
     * 
475
     * @param mixed $input the raw input
476
     * @return boolean|string echoes the mail address, or FALSE if bogus
477
     */
478
    public function email($input)
479
    {
480
481
        if (filter_var($this->string($input), FILTER_VALIDATE_EMAIL)) {
482
            return $input;
483
        }
484
        // if we get here, it's bogus
485
        return FALSE;
486
    }
487
488
    /**
489
     * is this a well-formed SMS number? Light massaging - leading + will be removed
490
     * @param string $input the raw input
491
     * @return boolean|string
492
     */
493
    public function sms($input)
494
    {
495
        $number = str_replace(' ', '', str_replace(".", "", str_replace("+", "", $input)));
496
        if (!is_numeric($number)) {
497
            return FALSE;
498
        }
499
        return $number;
500
    }
501
502
    /**
503
     * Is this is a language we support? If not, sanitise to our configured default language.
504
     * 
505
     * @param mixed $input the candidate language identifier
506
     * @return string
507
     * @throws Exception
508
     */
509
    public function supportedLanguage($input)
510
    {
511
        if (!array_key_exists($input, \config\Master::LANGUAGES)) {
512
            return \config\Master::APPEARANCE['defaultlocale'];
513
        }
514
        // otherwise, use the inversion trick to convince Scrutinizer that this is
515
        // a vetted value
516
        $retval = array_search(\config\Master::LANGUAGES[$input], \config\Master::LANGUAGES);
517
        if ($retval === FALSE) {
518
            throw new Exception("Impossible: the value we are searching for does exist, because we reference it directly.");
519
        }
520
        return $retval;
521
    }
522
523
    /**
524
     * Makes sure we are not receiving a bogus option name. The called function throws
525
     * an assertion if the name is not known.
526
     * 
527
     * @param mixed $input the unvetted option name
528
     * @return string
529
     */
530
    public function optionName($input)
531
    {
532
        $object = \core\Options::instance();
533
        return $object->assertValidOptionName($input);
534
    }
535
536
    /**
537
     * Checks to see if the input is a valid image of sorts
538
     * 
539
     * @param mixed $binary blob that may or may not be a parseable image
540
     * @return boolean
541
     */
542
    public function image($binary)
543
    {
544
        $image = new \Imagick();
545
        try {
546
            $image->readImageBlob($binary);
547
        } catch (\ImagickException $exception) {
548
            echo "Error" . $exception->getMessage();
549
            return FALSE;
550
        }
551
        // image survived the sanity check
552
        return TRUE;
553
    }
554
555
    /**
556
     * searches for values in GET and POST, and filters the value according to
557
     * which kind of data is expected
558
     * 
559
     * @param string $varName name of the variable in GET/POST
560
     * @param string $filter  which type of filter to apply (safe_text / int)
561
     * @return NULL|string|integer the returned value
562
     */
563
    public function simpleInputFilter($varName, $filter)
564
    {
565
        $safeText = ["options" => ["regexp" => "/^[\w\d-]+$/"]];
566
        switch ($filter) {
567
            case 'safe_text':
568
                $out = filter_input(INPUT_GET, $varName, FILTER_VALIDATE_REGEXP, $safeText) ?? filter_input(INPUT_POST, $varName, FILTER_VALIDATE_REGEXP, $safeText);
569
                break;
570
            case 'int':
571
                $out = filter_input(INPUT_GET, $varName, FILTER_VALIDATE_INT) ?? filter_input(INPUT_POST, $varName, FILTER_VALIDATE_INT);
572
                break;
573
            default:
574
                $out = NULL;
575
                break;
576
        }
577
        if ($out === false) { // an error occured during the filter_input runs; make this NULL instead then
578
            $out = NULL;
579
        }
580
        return $out;
581
    }
582
583
}
584