Passed
Push — master ( 09cfdd...94b89f )
by Stefan
07:21 queued 03:30
created

InputValidation::consortiumOI()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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