Issues (173)

Security Analysis    13 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting (3)
Response Splitting can be used to send arbitrary responses.
  File Manipulation (6)
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting (1)
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

web/lib/admin/OptionParser.php (2 issues)

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\admin;
24
25
use Exception;
26
27
?>
28
<?php
29
30
/**
31
 * This class parses HTML field input from POST and FILES and extracts valid and authorised options to be set.
32
 * 
33
 * @author Stefan Winter <[email protected]>
34
 */
35
class OptionParser extends \core\common\Entity {
36
37
    /**
38
     * an instance of the InputValidation class which we use heavily for syntax checks.
39
     * 
40
     * @var \web\lib\common\InputValidation
41
     */
42
    private $validator;
43
44
    /**
45
     * an instance of the UIElements() class to draw some UI widgets from.
46
     * 
47
     * @var UIElements
48
     */
49
    private $uiElements;
50
51
    /**
52
     * a handle for the Options singleton
53
     * 
54
     * @var \core\Options
55
     */
56
    private $optioninfoObject;
57
58
    /**
59
     * initialises the various handles.
60
     */
61
    public function __construct() {
62
        $this->validator = new \web\lib\common\InputValidation();
63
        $this->uiElements = new UIElements();
64
        $this->optioninfoObject = \core\Options::instance();
65
        $this->loggerInstance = new \core\common\Logging();
66
    }
67
68
    /**
69
     * Verifies whether an incoming upload was actually valid data
70
     * 
71
     * @param string $optiontype     for which option was the data uploaded
72
     * @param string $incomingBinary the uploaded data
73
     * @return array ['result'=>boolean whether the data was valid, 'details'=>string description of the problem]
74
     */
75
        private function checkUploadSanity(string $optiontype, string $incomingBinary) {
76
        switch ($optiontype) {
77
            case "general:logo_file":
78
            case "fed:logo_file":
79
            case "internal:logo_from_url":
80
                $result = $this->validator->image($incomingBinary);
81
                // we check logo_file with ImageMagick
82
                if ($result) {
83
                    return ['result'=>TRUE, 'details'=>''];
84
                }
85
                return ['result'=>FALSE, 'details'=>_('unsupported image type')];
86
            case "eap:ca_file":
87
            // fall-through intended: both CA types are treated the same
88
            case "fed:minted_ca_file":
89
                // echo "Checking $optiontype with file $filename";
90
                $cert = (new \core\common\X509)->processCertificate($incomingBinary);
91
                if ($cert !== FALSE) { // could also be FALSE if it was incorrect incoming data
92
                    $fail = false;
93
                    if ($cert['full_details']['type'] == 'server') {
94
                        $reason = _("%s - server certificate (<a href='%s'>more info</a>)");
95
                        $fail = true;
96
                    } elseif($cert['basicconstraints_set'] === 0) {
97
                        $reason = _("%s - missing required CA extensions (<a href='%s'>more info</a>)");
98
                        $fail = true;
99
                    }    
100
                    if ($fail) {
101
                        if (\config\ConfAssistant::CERT_GUIDELINES === '') {
102
                            $ret_val = sprintf(preg_replace('/\(<a.*>\)/', '', $reason), $cert['full_details']['subject']['CN']);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $reason does not seem to be defined for all execution paths leading up to this point.
Loading history...
103
                        } else {
104
                            $ret_val = sprintf($reason, $cert['full_details']['subject']['CN'], \config\ConfAssistant::CERT_GUIDELINES);
105
                        }
106
                        return ['result'=>FALSE, 'details'=>$ret_val];
107
                    }                
108
                    return ['result'=>TRUE, 'details'=>''];
109
                }
110
                // the certificate seems broken
111
                return ['result'=>FALSE, 'details'=>''];
112
            case "support:info_file":
113
                $info = new \finfo();
114
                $filetype = $info->buffer($incomingBinary, FILEINFO_MIME_TYPE);
115
116
                // we only take plain text files in UTF-8!
117
                if ($filetype == "text/plain" && iconv("UTF-8", "UTF-8", $incomingBinary) !== FALSE) {
118
                    return ['result'=>TRUE, ''];
119
                }
120
                return ['result'=>FALSE, 'details'=>_("incorrect file type - must be UTF8 text")];
121
            case "media:openroaming": // and any other enum_* data type actually
122
                $optionClass = \core\Options::instance();
123
                $optionProps = $optionClass->optionType($optiontype);
124
                $allowedValues = explode(',', substr($optionProps["flags"], 7));
125
                if (in_array($incomingBinary,$allowedValues))  {
126
                    return ['result'=>TRUE, 'details'=>''];
127
                }
128
                return ['result'=>FALSE, 'details'=>''];
129
            default:
130
                return ['result'=>FALSE, 'details'=>''];
131
        }
132
    }
133
134
    /**
135
     * Known-good options are sometimes converted, this function takes care of that.
136
     * 
137
     * Cases in point:
138
     * - CA import by URL reference: fetch cert from URL and store it as CA file instead
139
     * - Logo import by URL reference: fetch logo from URL and store it as logo file instead
140
     * - CA file: mangle the content so that *only* the valid content remains (raw input may contain line breaks or spaces which are valid, but some supplicants choke upon)
141
     * 
142
     * @param array $options the list of options we got
143
     * @param array $good    by-reference: the future list of actually imported options
144
     * @param array $bad     by-reference: the future list of submitted but rejected options
145
     * @return array the options, post-processed
146
     */
147
    private function postProcessValidAttributes(array $options, array &$good, array &$bad) {
148
        foreach ($options as $index => $iterateOption) {
149
            foreach ($iterateOption as $name => $optionPayload) {
150
                switch ($name) {
151
                    case "eap:ca_url": // eap:ca_url becomes eap:ca_file by downloading the file
152
                        $finalOptionname = "eap:ca_file";
153
                    // intentional fall-through, treatment identical to logo_url
154
                    case "general:logo_url": // logo URLs become logo files by downloading the file
155
                        $finalOptionname = $finalOptionname ?? "general:logo_file";
156
                        if (empty($optionPayload['content'])) {
157
                            break;
158
                        }
159
                        $bindata = \core\common\OutsideComm::downloadFile($optionPayload['content']);
160
                        unset($options[$index]);
161
                        if ($bindata === FALSE) {
162
                            $bad[] = ['type'=>$name, 'details'=>_("missing content")];
163
                            break;
164
                        }
165
                        if ($this->checkUploadSanity($finalOptionname, $bindata)['result']) {
166
                            $good[] = $name;
167
                            $options[] = [$finalOptionname => ['lang' => NULL, 'content' => base64_encode($bindata)]];
168
                        } else {
169
                            $bad[] = ['type'=>$name, 'details'=>''];
170
                        }
171
                        break;
172
                    case "eap:ca_file":
173
                    case "fed:minted_ca_file":
174
                        // CA files get split (PEM files can contain more than one CA cert)
175
                        // the data being processed here is always "good": 
176
                        // if it was eap:ca_file initially then its sanity was checked in step 1;
177
                        // if it was eap:ca_url then it was checked after we downloaded it
178
                        if (empty($optionPayload['content'])) {
179
                            break;
180
                        }
181
                        if (preg_match('/^ROWID-/', $optionPayload['content'])) {
182
                            // accounted for, already in DB
183
                            $good[] = $name;
184
                            break;
185
                        }
186
                        $content = base64_decode($optionPayload['content']);
187
                        unset($options[$index]);
188
                        $x509 = new \core\common\X509();
189
                        $cAFiles = $x509->splitCertificate($content);
190
                        foreach ($cAFiles as $cAFile) {
191
                            $options[] = [$name => ['lang' => NULL, 'content' => base64_encode($x509->pem2der($cAFile))]];
192
                        }
193
                        $good[] = $name;
194
                        break;
195
                    default:
196
                        $good[] = $name; // all other options were checked and are sane in step 1 already
197
                        break;
198
                }
199
            }
200
        }
201
        return $options;
202
    }
203
204
    /**
205
     * extracts a coordinate pair from _POST (if any) and returns it in our 
206
     * standard attribute notation
207
     * 
208
     * @param array $postArray data as sent by POST
209
     * @param array $good      options which have been successfully parsed
210
     * @return array
211
     */
212
    private function postProcessCoordinates(array $postArray, array &$good) {
213
        if (!empty($postArray['geo_long']) && !empty($postArray['geo_lat'])) {
214
215
            $lat = $this->validator->coordinate($postArray['geo_lat']);
216
            $lon = $this->validator->coordinate($postArray['geo_long']);
217
            $good[] = ("general:geo_coordinates");
218
            return [0 => ["general:geo_coordinates" => ['lang' => NULL, 'content' => json_encode(["lon" => $lon, "lat" => $lat])]]];
219
        }
220
        return [];
221
    }
222
223
    /**
224
     * creates HTML code for a user-readable summary of the imports
225
     * @param array $good           list of actually imported options
226
     * @param array $bad            list of submitted but rejected options
227
     * @param array $mlAttribsWithC list of language-variant options
228
     * @return string HTML code
229
     */
230
    private function displaySummaryInUI(array $good, array $bad, array $mlAttribsWithC) {
231
        \core\common\Entity::intoThePotatoes();
232
        $retval = "";
233
        // don't do your own table - only the <tr>s here
234
        // list all attributes that were set correctly
235
        $listGood = array_count_values($good);
236
        $uiElements = new UIElements();
237
        foreach ($listGood as $name => $count) {
238
            /// number of times attribute is present, and its name
239
            /// Example: "5x Support E-Mail"
240
            $retval .= $this->uiElements->boxOkay(sprintf(_("%dx %s"), $count, $uiElements->displayName($name)));
241
        }
242
        // list all attributes that had errors
243
244
        foreach ($bad as $badInstance) {
245
            $details = $badInstance['details'] === '' ? '' : ' - '.$badInstance['details'];
246
            $retval .= $this->uiElements->boxError(sprintf(_("%s"), $uiElements->displayName($badInstance['type']).$details));
247
        }
248
        // list multilang without default
249
        foreach ($mlAttribsWithC as $attribName => $isitsetornot) {
250
            if ($isitsetornot == FALSE) {
251
                $retval .= $this->uiElements->boxWarning(sprintf(_("You did not set a 'default language' value for %s. This means we can only display this string for installers which are <strong>exactly</strong> in the language you configured. For the sake of all other languages, you may want to edit the profile again and populate the 'default/other' language field."), $uiElements->displayName($attribName)));
252
            }
253
        }
254
        \core\common\Entity::outOfThePotatoes();
255
        return $retval;
256
    }
257
258
    /**
259
     * Incoming data is in $_POST and possibly in $_FILES. Collate values into 
260
     * one array according to our name and numbering scheme.
261
     * 
262
     * @param array $postArray  _POST
263
     * @param array $filesArray _FILES
264
     * @return array
265
     */
266
    private function collateOptionArrays(array $postArray, array $filesArray) {
267
268
        $optionarray = $postArray['option'] ?? [];
269
        $valuearray = $postArray['value'] ?? [];
270
        $filesarray = $filesArray['value']['tmp_name'] ?? [];
271
272
        $iterator = array_merge($optionarray, $valuearray, $filesarray);
273
274
        return $iterator;
275
    }
276
277
    /**
278
     * The very end of the processing: clean input data gets sent to the database
279
     * for storage
280
     * 
281
     * @param mixed  $object            for which object are the options
282
     * @param array  $options           the options to store
283
     * @param array  $pendingattributes list of attributes which are already stored but may need to be deleted
284
     * @param string $device            when the $object is Profile, this indicates device-specific attributes
285
     * @param int    $eaptype           when the $object is Profile, this indicates eap-specific attributes
286
     * @return array list of attributes which were previously stored but are to be deleted now
287
     * @throws Exception
288
     */
289
    private function sendOptionsToDatabase($object, array $options, array $pendingattributes, string $device = NULL, int $eaptype = NULL) {
290
        $retval = [];
291
        foreach ($options as $iterateOption) {
292
            foreach ($iterateOption as $name => $optionPayload) {
293
                $optiontype = $this->optioninfoObject->optionType($name);
294
                // some attributes are in the DB and were only called by reference
295
                // keep those which are still referenced, throw the rest away
296
                if ($optiontype["type"] == \core\Options::TYPECODE_FILE && preg_match("/^ROWID-.*-([0-9]+)/", $optionPayload['content'], $retval)) {
297
                    unset($pendingattributes[$retval[1]]);
298
                    continue;
299
                }
300
                switch (get_class($object)) {
301
                    case 'core\\ProfileRADIUS':
302
                        if ($device !== NULL) {
303
                            $object->addAttributeDeviceSpecific($name, $optionPayload['lang'], $optionPayload['content'], $device);
304
                        } elseif ($eaptype !== NULL) {
305
                            $object->addAttributeEAPSpecific($name, $optionPayload['lang'], $optionPayload['content'], $eaptype);
306
                        } else {
307
                            $object->addAttribute($name, $optionPayload['lang'], $optionPayload['content']);
308
                        }
309
                        break;
310
                    case 'core\\IdP':
311
                    case 'core\\User':
312
                    case 'core\\Federation':
313
                    case 'core\\DeploymentManaged':
314
                        $object->addAttribute($name, $optionPayload['lang'], $optionPayload['content']);
315
                        break;
316
                    default:
317
                        throw new Exception("This type of object can't have options that are parsed by this file!");
318
                }
319
            }
320
        }
321
        return $pendingattributes;
322
    }
323
324
    /** many of the content check cases in sanitiseInputs condense due to
325
     *  identical treatment except which validator function to call and 
326
     *  where in POST the content is.
327
     * 
328
     * This is a map between datatype and validation function.
329
     * 
330
     * @var array
331
     */
332
    private const VALIDATOR_FUNCTIONS = [
333
        \core\Options::TYPECODE_TEXT => ["function" => "string", "field" => \core\Options::TYPECODE_TEXT, "extraarg" => [TRUE]],
334
        \core\Options::TYPECODE_COORDINATES => ["function" => "coordJsonEncoded", "field" => \core\Options::TYPECODE_TEXT, "extraarg" => []],
335
        \core\Options::TYPECODE_BOOLEAN => ["function" => "boolean", "field" => \core\Options::TYPECODE_BOOLEAN, "extraarg" => []],
336
        \core\Options::TYPECODE_INTEGER => ["function" => "integer", "field" => \core\Options::TYPECODE_INTEGER, "extraarg" => []],
337
    ];
338
339
    /**
340
     * filters the input to find syntactically correctly submitted attributes
341
     * 
342
     * @param array $listOfEntries list of POST and FILES entries
343
     * @return array sanitised list of options
344
     * @throws Exception
345
     */
346
    private function sanitiseInputs(array $listOfEntries) {
347
        $retval = [];
348
        $bad = [];
349
        $multilangAttrsWithC = [];
350
        foreach ($listOfEntries as $objId => $objValueRaw) {
351
// pick those without dash - they indicate a new value        
352
            if (preg_match('/^S[0123456789]*$/', $objId) != 1) { // no match
353
                continue;
354
            }
355
            $objValue = $this->validator->optionName(preg_replace('/#.*$/', '', $objValueRaw));
356
            $optioninfo = $this->optioninfoObject->optionType($objValue);
357
            $languageFlag = NULL;
358
            if ($optioninfo["flag"] == "ML") {
359
                if (!isset($listOfEntries["$objId-lang"])) {
360
                    $bad[] = ['type'=>$objValue, 'details'=>''];
361
                    continue;
362
                }
363
                $languageFlag = $this->validator->string($listOfEntries["$objId-lang"]);
364
                $this->determineLanguages($objValue, $listOfEntries["$objId-lang"], $multilangAttrsWithC);
365
            }
366
367
            switch ($optioninfo["type"]) {
368
                case \core\Options::TYPECODE_TEXT:
369
                case \core\Options::TYPECODE_COORDINATES:
370
                case \core\Options::TYPECODE_INTEGER:
371
                    $varName = $listOfEntries["$objId-" . self::VALIDATOR_FUNCTIONS[$optioninfo['type']]['field']];
372
                    if (!empty($varName)) {
373
                        $content = call_user_func_array([$this->validator, self::VALIDATOR_FUNCTIONS[$optioninfo['type']]['function']], array_merge([$varName], self::VALIDATOR_FUNCTIONS[$optioninfo['type']]['extraarg']));
374
                        break;
375
                    }
376
                    continue 2;
377
                case \core\Options::TYPECODE_BOOLEAN:
378
                    $varName = $listOfEntries["$objId-" . \core\Options::TYPECODE_BOOLEAN];
379
                    if (!empty($varName)) {
380
                        $contentValid = $this->validator->boolean($varName);
381
                        if ($contentValid) {
382
                            $content = "on";
383
                        } else {
384
                            $bad[] = ['type'=>$objValue, 'details'=>''];
385
                            continue 2;
386
                        }
387
                        break;
388
                    }
389
                    continue 2;
390
                case \core\Options::TYPECODE_STRING:
391
                    $previsionalContent = $listOfEntries["$objId-" . \core\Options::TYPECODE_STRING];
392
                    if (!empty(trim($previsionalContent))) {
393
                        $content = $this->furtherStringChecks($objValue, $previsionalContent, $bad);
394
                        if ($content === FALSE) {
395
                            continue 2;
396
                        }
397
                        break;
398
                    }
399
                    continue 2;
400
                    
401
                case \core\Options::TYPECODE_ENUM_OPENROAMING:
402
                    $previsionalContent = $listOfEntries["$objId-" . \core\Options::TYPECODE_ENUM_OPENROAMING];
403
                    if (!empty($previsionalContent)) {
404
                        $content = $this->furtherStringChecks($objValue, $previsionalContent, $bad);
405
                        if ($content === FALSE) {
406
                            continue 2;
407
                        }
408
                        break;
409
                    }
410
                    continue 2;    
411
                case \core\Options::TYPECODE_FILE:
412
                    // this is either actually an uploaded file, or a reference to a DB entry of a previously uploaded file
413
                    $reference = $listOfEntries["$objId-" . \core\Options::TYPECODE_STRING];
414
                    if (!empty($reference)) { // was already in, by ROWID reference, extract
415
                        // ROWID means it's a multi-line string (simple strings are inline in the form; so allow whitespace)
416
                        $content = $this->validator->string(urldecode($reference), TRUE);
417
                        break;
418
                    }
419
                    $fileName = $listOfEntries["$objId-" . \core\Options::TYPECODE_FILE] ?? "";
420
                    if ($fileName != "") { // let's do the download
421
                        $rawContent = \core\common\OutsideComm::downloadFile("file:///" . $fileName);
422
                        $sanity = $this->checkUploadSanity($objValue, $rawContent);
0 ignored issues
show
It seems like $rawContent can also be of type false; however, parameter $incomingBinary of web\lib\admin\OptionParser::checkUploadSanity() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

422
                        $sanity = $this->checkUploadSanity($objValue, /** @scrutinizer ignore-type */ $rawContent);
Loading history...
423
                        if ($rawContent === FALSE || !$sanity['result']) {
424
                            $bad[] = ['type'=>$objValue, 'details'=>$sanity['details']];
425
                            continue 2;
426
                        }
427
                        $content = base64_encode($rawContent);
428
                        break;
429
                    }
430
                    continue 2;
431
                default:
432
                    throw new Exception("Internal Error: Unknown option type " . $objValue . "!");
433
            }
434
            // lang can be NULL here, if it's not a multilang attribute, or a ROWID reference. Never mind that.
435
            $retval[] = ["$objValue" => ["lang" => $languageFlag, "content" => $content]];
436
        }
437
        return [$retval, $multilangAttrsWithC, $bad];
438
    }
439
440
    /**
441
     * find out which languages were submitted, and whether a default language was in the set
442
     * @param string $attribute           the name of the attribute we are looking at
443
     * @param string $languageFlag        which language flag was submitted
444
     * @param array  $multilangAttrsWithC by-reference: add to this if we found a C language variant
445
     * @return void
446
     */
447
    private function determineLanguages($attribute, $languageFlag, &$multilangAttrsWithC) {
448
        if (!isset($multilangAttrsWithC[$attribute])) { // on first sight, initialise the attribute as "no C language set"
449
            $multilangAttrsWithC[$attribute] = FALSE;
450
        }
451
        if ($languageFlag == "") { // user forgot to select a language
452
            $languageFlag = "C";
453
        }
454
        // did we get a C language? set corresponding value to TRUE
455
        if ($languageFlag == "C") {
456
            $multilangAttrsWithC[$attribute] = TRUE;
457
        }
458
    }
459
460
    /**
461
     * 
462
     * @param string $attribute          which attribute was sent?
463
     * @param string $previsionalContent which content was sent?
464
     * @param array  $bad                list of malformed attributes, by-reference
465
     * @return string|false FALSE if value is not in expected format, else the content itself
466
     */
467
    private function furtherStringChecks($attribute, $previsionalContent, &$bad) {
468
        $content = FALSE;
469
        switch ($attribute) {
470
            case "media:consortium_OI":
471
                $content = $this->validator->consortiumOI($previsionalContent);
472
                if ($content === FALSE) {
473
                    $bad[] = ['type'=>$attribute, 'details'=>''];
474
                    return FALSE;
475
                }
476
                break;
477
            case "media:remove_SSID":
478
                $content = $this->validator->string($previsionalContent);
479
                if ($content == "eduroam") {
480
                    $bad[] = ['type'=>$attribute, 'details'=>''];
481
                    return FALSE;
482
                }
483
                break;
484
            case "media:force_proxy":
485
                $content = $this->validator->string($previsionalContent);
486
                $serverAndPort = explode(':', strrev($content), 2);
487
                if (count($serverAndPort) != 2) {
488
                    $bad[] = ['type'=>$attribute, 'details'=>''];
489
                    return FALSE;
490
                }
491
                $port = strrev($serverAndPort[0]);
492
                if (!is_numeric($port)) {
493
                    $bad[] = ['type'=>$attribute, 'details'=>''];
494
                    return FALSE;
495
                }
496
                break;
497
            case "support:url":
498
                $content = $this->validator->string($previsionalContent);
499
                if (preg_match("/^http/", $content) != 1) {
500
                    $bad[] = ['type'=>$attribute, 'details'=>''];
501
                    return FALSE;
502
                }
503
                break;
504
            case "support:email":
505
                $content = $this->validator->email($previsionalContent);
506
                if ($content === FALSE) {
507
                    $bad[] = ['type'=>$attribute, 'details'=>''];
508
                    return FALSE;
509
                }
510
                break;
511
            case "managedsp:operatorname":
512
                $content = $previsionalContent;
513
                if (!preg_match("/^1.*\..*/", $content)) {
514
                    $bad[] = ['type'=>$attribute, 'details'=>''];
515
                    return FALSE;
516
                }
517
                break;
518
            default:
519
                $content = $this->validator->string($previsionalContent);
520
                break;
521
        }
522
        return $content;
523
    }
524
525
    /**
526
     * The main function: takes all HTML field inputs, makes sense of them and stores valid data in the database
527
     * 
528
     * @param mixed  $object     The object for which attributes were submitted
529
     * @param array  $postArray  incoming attribute names and values as submitted with $_POST
530
     * @param array  $filesArray incoming attribute names and values as submitted with $_FILES
531
     * @param int    $eaptype    for eap-specific attributes (only used where $object is a ProfileRADIUS instance)
532
     * @param string $device     for device-specific attributes (only used where $object is a ProfileRADIUS instance)
533
     * @return string text to be displayed in UI with the summary of attributes added
534
     * @throws Exception
535
     */
536
    public function processSubmittedFields($object, array $postArray, array $filesArray, int $eaptype = NULL, string $device = NULL) {
537
        $good = [];
538
        // Step 1: collate option names, option values and uploaded files (by 
539
        // filename reference) into one array for later handling
540
541
        $iterator = $this->collateOptionArrays($postArray, $filesArray);
542
543
        // Step 2: sieve out malformed input
544
        // $multilangAttrsWithC is a helper array to keep track of multilang 
545
        // options that were set in a specific language but are not 
546
        // accompanied by a "default" language setting
547
        // if there are some without C by the end of processing, we need to warn
548
        // the admin that this attribute is "invisible" in certain languages
549
        // attrib_name -> boolean
550
        // $bad contains the attributes which failed input validation
551
552
        list($cleanData, $multilangAttrsWithC, $bad) = $this->sanitiseInputs($iterator);
553
554
        // Step 3: now we have clean input data. Some attributes need special care:
555
        // URL-based attributes need to be downloaded to get their actual content
556
        // CA files may need to be split (PEM can contain multiple CAs 
557
558
        $optionsStep2 = $this->postProcessValidAttributes($cleanData, $good, $bad);
559
560
        // Step 4: coordinates do not follow the usual POST array as they are 
561
        // two values forming one attribute; extract those two as an extra step
562
563
        $options = array_merge($optionsStep2, $this->postProcessCoordinates($postArray, $good));
564
        
565
        // Step 5: push all the received options to the database. Keep mind of 
566
        // the list of existing database entries that are to be deleted.
567
        // 5a: first deletion step: purge all old content except file-based attributes;
568
        //     then take note of which file-based attributes are now stale
569
        if ($device === NULL && $eaptype === NULL) {
570
            $remaining = $object->beginflushAttributes();
571
            $killlist = $this->sendOptionsToDatabase($object, $options, $remaining);
572
        } elseif ($device !== NULL) {
573
            $remaining = $object->beginFlushMethodLevelAttributes(0, $device);
574
            $killlist = $this->sendOptionsToDatabase($object, $options, $remaining, $device);
575
        } else {
576
            $remaining = $object->beginFlushMethodLevelAttributes($eaptype, "");
577
            $killlist = $this->sendOptionsToDatabase($object, $options, $remaining, NULL, $eaptype);
578
        }
579
        // 5b: finally, kill the stale file-based attributes which are not wanted any more.
580
        $object->commitFlushAttributes($killlist);
581
582
        // finally: return HTML code that gives feedback about what we did. 
583
        // In some cases, callers won't actually want to display it; so simply
584
        // do not echo the return value. Reasons not to do this is if we working
585
        // e.g. from inside an overlay
586
        return $this->displaySummaryInUI($good, $bad, $multilangAttrsWithC);
587
    }
588
589
}
590