Passed
Push — master ( 54bc3c...3014eb )
by Stefan
07:27
created

InputValidation::existingProfile()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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