Passed
Branch master (d745a6)
by Stefan
08:19
created

InputValidation::simpleInputFilter()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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