Passed
Push — master ( 28eddd...1e0b05 )
by Stefan
08:22
created

InputValidation::optionName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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