Test Setup Failed
Push — master ( d62009...b98104 )
by Stefan
21:57
created

AbstractProfile::setOpenRoamingReadinessInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
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
/**
24
 * This file contains the AbstractProfile class. It contains common methods for
25
 * both RADIUS/EAP profiles and SilverBullet profiles
26
 *
27
 * @author Stefan Winter <[email protected]>
28
 * @author Tomasz Wolniewicz <[email protected]>
29
 *
30
 * @package Developer
31
 *
32
 */
33
34
namespace core;
35
36
use \Exception;
37
38
/**
39
 * This class represents an EAP Profile.
40
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
41
 * one can set attribute in the Profile directly. If there is a conflict between
42
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
43
 * 
44
 * @author Stefan Winter <[email protected]>
45
 * @author Tomasz Wolniewicz <[email protected]>
46
 *
47
 * @license see LICENSE file in root directory
48
 *
49
 * @package Developer
50
 */
51
abstract class AbstractProfile extends EntityWithDBProperties
52
{
53
54
    const HIDDEN = -1;
55
    const AVAILABLE = 0;
56
    const UNAVAILABLE = 1;
57
    const INCOMPLETE = 2;
58
    const NOTCONFIGURED = 3;
59
    const PROFILETYPE_RADIUS = "RADIUS";
60
    const PROFILETYPE_SILVERBULLET = "SILVERBULLET";
61
    public const SERVERNAME_ADDED = 2;
62
    public const CA_ADDED = 3;
63
    public const CA_CLASH_ADDED = 4;
64
65
    /**
66
     * DB identifier of the parent institution of this profile
67
     * @var integer
68
     */
69
    public $institution;
70
71
    /**
72
     * name of the parent institution of this profile in the current language
73
     * @var string
74
     */
75
    public $instName;
76
77
    /**
78
     * realm of this profile (empty string if unset)
79
     * @var string
80
     */
81
    public $realm;
82
83
    /**
84
     * This array holds the supported EAP types (in object representation). 
85
     * 
86
     * They are not synced against the DB after instantiation.
87
     * 
88
     * @var array
89
     */
90
    protected $privEaptypes;
91
92
    /**
93
     * number of profiles of the IdP this profile is attached to
94
     * 
95
     * @var integer
96
     */
97
    protected $idpNumberOfProfiles;
98
99
    /**
100
     * IdP-wide attributes of the IdP this profile is attached to
101
     * 
102
     * @var array
103
     */
104
    protected $idpAttributes;
105
106
    /**
107
     * Federation level attributes that this profile is attached to via its IdP
108
     * 
109
     * @var array
110
     */
111
    protected $fedAttributes;
112
113
    /**
114
     * This class also needs to handle frontend operations, so needs its own
115
     * access to the FRONTEND datbase. This member stores the corresponding 
116
     * handle.
117
     * 
118
     * @var DBConnection
119
     */
120
    protected $frontendHandle;
121
122
    /**
123
     * readiness levels for OpenRoaming column in profiles)
124
     */
125
    const OVERALL_OPENROAMING_LEVEL_NO = 4;
126
    const OVERALL_OPENROAMING_LEVEL_GOOD = 3;
127
    const OVERALL_OPENROAMING_LEVEL_NOTE = 2;
128
    const OVERALL_OPENROAMING_LEVEL_WARN = 1;
129
    const OVERALL_OPENROAMING_LEVEL_ERROR = 0;
130
    
131
    /**
132
     *  generates a detailed log of which installer was downloaded
133
     * 
134
     * @param int    $idpIdentifier the IdP identifier
135
     * @param int    $profileId     the Profile identifier
136
     * @param string $deviceId      the Device identifier
137
     * @param string $area          the download area (user, silverbullet, admin)
138
     * @param string $lang          the language of the installer
139
     * @param int    $eapType       the EAP type of the installer
140
     * @return void
141
     * @throws Exception
142
     */
143
    protected function saveDownloadDetails($idpIdentifier, $profileId, $deviceId, $area, $lang, $eapType)
144
    {
145
        if (\config\Master::PATHS['logdir']) {
146
            $file = fopen(\config\Master::PATHS['logdir'] . "/download_details.log", "a");
147
            if ($file === FALSE) {
148
                throw new Exception("Unable to open file for append: $file");
149
            }
150
            fprintf($file, "%-015s;%d;%d;%s;%s;%s;%d\n", microtime(TRUE), $idpIdentifier, $profileId, $deviceId, $area, $lang, $eapType);
151
            fclose($file);
152
        }
153
    }
154
155
    /**
156
     * checks if security-relevant parameters have changed
157
     * 
158
     * @param AbstractProfile $old old instantiation of a profile to compare against
159
     * @param AbstractProfile $new new instantiation of a profile 
160
     * @return array there are never any user-induced changes in SB
161
     */
162
    public static function significantChanges($old, $new)
163
    {
164
        $retval = [];
165
        // check if a CA was added
166
        $x509 = new common\X509();
167
        $baselineCA = [];
168
        $baselineCApublicKey = [];
169
        foreach ($old->getAttributes("eap:ca_file") as $oldCA) {
170
            $ca = $x509->processCertificate($oldCA['value']);
171
            $baselineCA[$ca['sha1']] = $ca['name'];
172
            $baselineCApublicKey[$ca['sha1']] = $ca['full_details']['public_key'];
173
        }
174
        // remove the new ones that are identical to the baseline
175
        foreach ($new->getAttributes("eap:ca_file") as $newCA) {
176
            $ca = $x509->processCertificate($newCA['value']);
177
            if (array_key_exists($ca['sha1'], $baselineCA) || $ca['root'] != 1) {
178
                // do nothing; we assume here that SHA1 doesn't clash
179
                continue;
180
            }
181
            // check if a CA with identical DN was added - alert NRO if so
182
            $foundSHA1 = array_search($ca['name'], $baselineCA);
183
            if ($foundSHA1 !== FALSE) {
184
                // but only if the public key does not match
185
                if ($baselineCApublicKey[$foundSHA1] === $ca['full_details']['public_key']) {
186
                    continue;
187
                }
188
                $retval[AbstractProfile::CA_CLASH_ADDED] .= "#SHA1 for CA with DN '".$ca['name']."' has SHA1 fingerprints (pre-existing) "./** @scrutinizer ignore-type */ array_search($ca['name'], $baselineCA)." and (added) ".$ca['sha1'];
189
            } else {
190
                $retval[AbstractProfile::CA_ADDED] .= "#CA with DN '"./** @scrutinizer ignore-type */ print_r($ca['name'], TRUE)."' and SHA1 fingerprint ".$ca['sha1']." was added as trust anchor";
191
            }
192
        }
193
        // check if a servername was added
194
        $baselineNames = [];
195
        foreach ($old->getAttributes("eap:server_name") as $oldName) {
196
            $baselineNames[] = $oldName['value'];
197
        }
198
        foreach ($new->getAttributes("eap:server_name") as $newName) {
199
            if (!in_array($newName['value'], $baselineNames)) {
200
                $retval[AbstractProfile::SERVERNAME_ADDED] .= "#New server name '" . $newName['value'] . "' added";
201
            }
202
        }
203
        return $retval;
204
    }
205
206
    /**
207
     * Takes note of the OpenRoaming participation and conformance level
208
     * 
209
     * @param int $level the readiness level, as determined by RFC7585Tests
210
     * @return void
211
     */
212
    public function setOpenRoamingReadinessInfo(int $level)
213
    {
214
            $this->databaseHandle->exec("UPDATE profile SET openroaming = ? WHERE profile_id = ?", "ii", $level, $this->identifier);
215
    }
216
217
    /**
218
     * each profile has supported EAP methods, so get this from DB, Silver Bullet has one
219
     * static EAP method.
220
     * 
221
     * @return array list of supported EAP methods
222
     */
223
    protected function fetchEAPMethods()
224
    {
225
        $eapMethod = $this->databaseHandle->exec("SELECT eap_method_id 
226
                                                        FROM supported_eap supp 
227
                                                        WHERE supp.profile_id = $this->identifier 
228
                                                        ORDER by preference");
229
        $eapTypeArray = [];
230
        // SELECTs never return a boolean, it's always a resource
231
        while ($eapQuery = (mysqli_fetch_object(/** @scrutinizer ignore-type */ $eapMethod))) {
232
            $eaptype = new common\EAP($eapQuery->eap_method_id);
233
            $eapTypeArray[] = $eaptype;
234
        }
235
        $this->loggerInstance->debug(4, "This profile supports the following EAP types:\n" . /** @scrutinizer ignore-type */ print_r($eapTypeArray, true));
236
        return $eapTypeArray;
237
    }
238
239
    /**
240
     * Class constructor for existing profiles (use IdP::newProfile() to actually create one). Retrieves all attributes and 
241
     * supported EAP types from the DB and stores them in the priv_ arrays.
242
     * 
243
     * sub-classes need to set the property $realm, $name themselves!
244
     * 
245
     * @param int $profileIdRaw identifier of the profile in the DB
246
     * @param IdP $idpObject    optionally, the institution to which this Profile belongs. Saves the construction of the IdP instance. If omitted, an extra query and instantiation is executed to find out.
247
     * @throws Exception
248
     */
249
    public function __construct($profileIdRaw, $idpObject = NULL)
250
    {
251
        $this->databaseType = "INST";
252
        parent::__construct(); // we now have access to our INST database handle and logging
253
        $handle = DBConnection::handle("FRONTEND");
254
        if ($handle instanceof DBConnection) {
255
            $this->frontendHandle = $handle;
256
        } else {
257
            throw new Exception("This database type is never an array!");
258
        }
259
        $profile = $this->databaseHandle->exec("SELECT inst_id FROM profile WHERE profile_id = ?", "i", $profileIdRaw);
260
        // SELECT always yields a resource, never a boolean
261
        if ($profile->num_rows == 0) {
262
            $this->loggerInstance->debug(2, "Profile $profileIdRaw not found in database!\n");
263
            throw new Exception("Profile $profileIdRaw not found in database!");
264
        }
265
        $this->identifier = $profileIdRaw;
266
        $profileQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $profile);
267
        if (!($idpObject instanceof IdP)) {
268
            $this->institution = $profileQuery->inst_id;
269
            $idp = new IdP($this->institution);
270
        } else {
271
            $idp = $idpObject;
272
            $this->institution = $idp->identifier;
273
        }
274
275
        $this->instName = $idp->name;
276
        $this->idpNumberOfProfiles = $idp->profileCount();
277
        $this->idpAttributes = $idp->getAttributes();
278
        $fedObject = new Federation($idp->federation);
279
        $this->fedAttributes = $fedObject->getAttributes();
280
        $this->loggerInstance->debug(3, "--- END Constructing new AbstractProfile object ... ---\n");
281
    }
282
283
    /**
284
     * find a profile, given its realm
285
     * 
286
     * @param string $realm the realm for which we are trying to find a profile
287
     * @return int|false the profile identifier, if any
288
     */
289
    public static function profileFromRealm($realm)
290
    {
291
        // static, need to create our own handle
292
        $handle = DBConnection::handle("INST");
293
        $execQuery = $handle->exec("SELECT profile_id FROM profile WHERE realm LIKE '%@$realm'");
294
        // a SELECT query always yields a resource, not a boolean
295
        if ($profileIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execQuery)) {
296
            return $profileIdQuery->profile_id;
297
        }
298
        return FALSE;
299
    }
300
301
    /**
302
     * Constructs the outer ID which should be used during realm tests. Obviously
303
     * can only do something useful if the realm is known to the system.
304
     * 
305
     * @return string the outer ID to use for realm check operations
306
     * @throws Exception
307
     */
308
    public function getRealmCheckOuterUsername()
309
    {
310
        $realm = $this->getAttributes("internal:realm")[0]['value'] ?? FALSE;
311
        if ($realm == FALSE) { // we can't really return anything useful here
312
            throw new Exception("Unable to construct a realmcheck username if the admin did not tell us the realm. You shouldn't have called this function in this context.");
313
        }
314
        if (count($this->getAttributes("internal:checkuser_outer")) > 0) {
315
            // we are supposed to use a specific outer username for checks, 
316
            // which is different from the outer username we put into installers
317
            return $this->getAttributes("internal:checkuser_value")[0]['value'] . "@" . $realm;
318
        }
319
        if (count($this->getAttributes("internal:use_anon_outer")) > 0) {
320
            // no special check username, but there is an anon outer ID for
321
            // installers - so let's use that one
322
            return $this->getAttributes("internal:anon_local_value")[0]['value'] . "@" . $realm;
323
        }
324
        // okay, no guidance on outer IDs at all - but we need *something* to
325
        // test with for the RealmChecks. So:
326
        return "@" . $realm;
327
    }
328
329
    /**
330
     * update the last_changed timestamp for this profile
331
     * 
332
     * @return void
333
     */
334
    public function updateFreshness()
335
    {
336
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE profile_id = $this->identifier");
337
    }
338
339
    /**
340
     * gets the last-modified timestamp (useful for caching "dirty" check)
341
     * 
342
     * @return string the date in string form, as returned by SQL
343
     */
344
    public function getFreshness()
345
    {
346
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM profile WHERE profile_id = $this->identifier");
347
        // SELECT always returns a resource, never a boolean
348
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
349
            return $freshnessQuery->last_change;
350
        }
351
    }
352
    /**
353
     * tests if the configurator needs to be regenerated
354
     * returns the configurator path or NULL if regeneration is required
355
     */
356
    /**
357
     * This function tests if the configurator needs to be regenerated 
358
     * (properties of the Profile may have changed since the last configurator 
359
     * generation).
360
     * SilverBullet will always return NULL here because all installers are new!
361
     * 
362
     * @param string $device device ID to check
363
     * @return mixed a string with the path to the configurator download, or NULL if it needs to be regenerated
364
     */
365
366
    /**
367
     * This function tests if the configurator needs to be regenerated (properties of the Profile may have changed since the last configurator generation).
368
     * 
369
     * @param string $device device ID to check
370
     * @return mixed a string with the path to the configurator download, or NULL if it needs to be regenerated
371
     */
372
    public function testCache($device)
373
    {
374
        $returnValue = ['cache' => NULL, 'mime' => NULL];
375
        $lang = $this->languageInstance->getLang();
376
        $result = $this->frontendHandle->exec("SELECT download_path, mime, UNIX_TIMESTAMP(installer_time) AS tm FROM downloads WHERE profile_id = ? AND device_id = ? AND lang = ?", "iss", $this->identifier, $device, $lang);
377
        // SELECT queries always return a resource, not a boolean
378
        if ($result && $cache = mysqli_fetch_object(/** @scrutinizer ignore-type */ $result)) {
379
            $execUpdate = $this->databaseHandle->exec("SELECT UNIX_TIMESTAMP(last_change) AS last_change FROM profile WHERE profile_id = $this->identifier");
380
            // SELECT queries always return a resource, not a boolean
381
            if ($lastChange = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execUpdate)->last_change) {
382
                if ($lastChange < $cache->tm) {
383
                    $this->loggerInstance->debug(4, "Installer cached:$cache->download_path\n");
384
                    $returnValue = ['cache' => $cache->download_path, 'mime' => $cache->mime];
385
                }
386
            }
387
        }
388
        return $returnValue;
389
    }
390
391
    /**
392
     * Updates database with new installer location. Actually does stuff when
393
     * caching is possible; is a noop if not
394
     * 
395
     * @param string $device         the device identifier string
396
     * @param string $path           the path where the new installer can be found
397
     * @param string $mime           the mime type of the new installer
398
     * @param int    $integerEapType the inter-representation of the EAP type that is configured in this installer
399
     * @return void
400
     */
401
    abstract public function updateCache($device, $path, $mime, $integerEapType);
402
403
    /** Toggle anonymous outer ID support.
404
     *
405
     * @param boolean $shallwe TRUE to enable outer identities (needs valid $realm), FALSE to disable
406
     * @return void
407
     */
408
    abstract public function setAnonymousIDSupport($shallwe);
409
410
    /**
411
     * Log a new download for our stats
412
     * 
413
     * @param string $device the device id string
414
     * @param string $area   either admin or user
415
     * @return boolean TRUE if incrementing worked, FALSE if not
416
     */
417
    public function incrementDownloadStats($device, $area)
418
    {
419
        if ($area == "admin" || $area == "user" || $area == "silverbullet") {
420
            $lang = $this->languageInstance->getLang();
421
            $this->frontendHandle->exec("INSERT INTO downloads (profile_id, device_id, lang, downloads_$area) VALUES (? ,?, ?, 1) ON DUPLICATE KEY UPDATE downloads_$area = downloads_$area + 1", "iss", $this->identifier, $device, $lang);
422
            // get eap_type from the downloads table
423
            $eapTypeQuery = $this->frontendHandle->exec("SELECT eap_type FROM downloads WHERE profile_id = ? AND device_id = ? AND lang = ?", "iss", $this->identifier, $device, $lang);
424
            // SELECT queries always return a resource, not a boolean
425
            if (!$eapTypeQuery || !$eapO = mysqli_fetch_object(/** @scrutinizer ignore-type */ $eapTypeQuery)) {
426
                $this->loggerInstance->debug(2, "Error getting EAP_type from the database\n");
427
            } else {
428
                if ($eapO->eap_type == NULL) {
429
                    $this->loggerInstance->debug(2, "EAP_type not set in the database\n");
430
                } else {
431
                    $this->saveDownloadDetails($this->institution, $this->identifier, $device, $area, $this->languageInstance->getLang(), $eapO->eap_type);
432
                }
433
            }
434
            return TRUE;
435
        }
436
        return FALSE;
437
    }
438
439
    /**
440
     * Retrieve current download stats from database, either for one specific device or for all devices
441
     * 
442
     * @param string $device the device id string
443
     * @return mixed user downloads of this profile; if device is given, returns the counter as int, otherwise an array with devicename => counter
444
     */
445
    public function getUserDownloadStats($device = NULL)
446
    {
447
        $columnName = "downloads_user";
448
        if ($this instanceof \core\ProfileSilverbullet) {
449
            $columnName = "downloads_silverbullet";
450
        }
451
        $returnarray = [];
452
        $numbers = $this->frontendHandle->exec("SELECT device_id, SUM($columnName) AS downloads_user FROM downloads WHERE profile_id = ? GROUP BY device_id", "i", $this->identifier);
453
        // SELECT queries always return a resource, not a boolean
454
        while ($statsQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $numbers)) {
455
            $returnarray[$statsQuery->device_id] = $statsQuery->downloads_user;
456
        }
457
        if ($device !== NULL) {
458
            if (isset($returnarray[$device])) {
459
                return $returnarray[$device];
460
            }
461
            return 0;
462
        }
463
        // we should pretty-print the device names
464
        $finalarray = [];
465
        $devlist = \devices\Devices::listDevices();
466
        foreach ($returnarray as $devId => $count) {
467
            if (isset($devlist[$devId])) {
468
                $finalarray[$devlist[$devId]['display']] = $count;
469
            }
470
        }
471
        return $finalarray;
472
    }
473
474
    /**
475
     * Deletes the profile from database and uninstantiates itself.
476
     * Works fine also for Silver Bullet; the first query will simply do nothing
477
     * because there are no stored options
478
     * 
479
     * @return void
480
     */
481
    public function destroy()
482
    {
483
        $this->databaseHandle->exec("DELETE FROM profile_option WHERE profile_id = $this->identifier");
484
        $this->databaseHandle->exec("DELETE FROM supported_eap WHERE profile_id = $this->identifier");
485
        $this->databaseHandle->exec("DELETE FROM profile WHERE profile_id = $this->identifier");
486
    }
487
488
    /**
489
     * Specifies the realm of this profile.
490
     * 
491
     * Forcefully type-hinting $realm parameter to string - Scrutinizer seems to
492
     * think that it can alternatively be an array<integer,?> which looks like a
493
     * false positive. If there really is an issue, let PHP complain about it at
494
     * runtime.
495
     * 
496
     * @param string $realm the realm (potentially with the local@ part that should be used for anonymous identities)
497
     * @return void
498
     */
499
    public function setRealm(string $realm)
500
    {
501
        $this->databaseHandle->exec("UPDATE profile SET realm = ? WHERE profile_id = ?", "si", $realm, $this->identifier);
502
        $this->realm = $realm;
503
    }
504
505
    /**
506
     * register new supported EAP method for this profile
507
     *
508
     * @param \core\common\EAP $type       The EAP Type, as defined in class EAP
509
     * @param int              $preference preference of this EAP Type. If a preference value is re-used, the order of EAP types of the same preference level is undefined.
510
     * @return void
511
     */
512
    public function addSupportedEapMethod(\core\common\EAP $type, $preference)
513
    {
514
        $eapInt = $type->getIntegerRep();
515
        $this->databaseHandle->exec("INSERT INTO supported_eap (profile_id, eap_method_id, preference) VALUES (?, ?, ?)", "iii", $this->identifier, $eapInt, $preference);
516
        $this->updateFreshness();
517
    }
518
519
    /**
520
     * Produces an array of EAP methods supported by this profile, ordered by preference
521
     * 
522
     * @param int $completeOnly if set and non-zero limits the output to methods with complete information
523
     * @return array list of EAP methods, (in object representation)
524
     */
525
    public function getEapMethodsinOrderOfPreference($completeOnly = 0)
526
    {
527
        $temparray = [];
528
529
        if ($completeOnly == 0) {
530
            return $this->privEaptypes;
531
        }
532
        foreach ($this->privEaptypes as $type) {
533
            if ($this->isEapTypeDefinitionComplete($type) === true) {
534
                $temparray[] = $type;
535
            }
536
        }
537
        return($temparray);
538
    }
539
540
    /**
541
     * Performs a sanity check for a given EAP type - did the admin submit enough information to create installers for him?
542
     * 
543
     * @param common\EAP $eaptype the EAP type
544
     * @return mixed TRUE if the EAP type is complete; an array of missing attribues if it's incomplete; FALSE if it's incomplete for other reasons
545
     */
546
    public function isEapTypeDefinitionComplete($eaptype)
547
    {
548
        if ($eaptype->needsServerCACert() && $eaptype->needsServerName()) {
549
            $missing = [];
550
            // silverbullet needs a support email address configured
551
            if ($eaptype->getIntegerRep() == common\EAP::INTEGER_SILVERBULLET && count($this->getAttributes("support:email")) == 0) {
552
                return ["support:email"];
553
            }
554
            $cnOption = $this->getAttributes("eap:server_name"); // cannot be set per device or eap type
555
            $caOption = $this->getAttributes("eap:ca_file"); // cannot be set per device or eap type
556
557
            if (count($caOption) > 0 && count($cnOption) > 0) {// see if we have at least one root CA cert
558
                foreach ($caOption as $oneCa) {
559
                    $x509 = new \core\common\X509();
560
                    $caParsed = $x509->processCertificate($oneCa['value']);
561
                    if ($caParsed['root'] == 1) {
562
                        return TRUE;
563
                    }
564
                }
565
                $missing[] = "eap:ca_file";
566
            }
567
            if (count($caOption) == 0) {
568
                $missing[] = "eap:ca_file";
569
            }
570
            if (count($cnOption) == 0) {
571
                $missing[] = "eap:server_name";
572
            }
573
            if (count($missing) == 0) {
574
                return TRUE;
575
            }
576
            return $missing;
577
        }
578
        return TRUE;
579
    }
580
581
    /**
582
     * list all devices marking their availabiblity and possible redirects
583
     *
584
     * @return array of device ids display names and their status
585
     */
586
    public function listDevices()
587
    {
588
        $returnarray = [];
589
        $redirect = $this->getAttributes("device-specific:redirect"); // this might return per-device ones or the general one
590
        // if it was a general one, we are done. Find out if there is one such
591
        // which has device = NULL
592
        $generalRedirect = NULL;
593
        foreach ($redirect as $index => $oneRedirect) {
594
            if ($oneRedirect["level"] == Options::LEVEL_PROFILE) {
595
                $generalRedirect = $index;
596
            }
597
        }
598
        if ($generalRedirect !== NULL) { // could be index 0
599
            return [['id' => '0', 'redirect' => $redirect[$generalRedirect]['value']]];
600
        }
601
        $preferredEap = $this->getEapMethodsinOrderOfPreference(1);
602
        $eAPOptions = [];
603
        foreach (\devices\Devices::listDevices() as $deviceIndex => $deviceProperties) {
604
            $factory = new DeviceFactory($deviceIndex);
605
            $dev = $factory->device;
606
            // find the attribute pertaining to the specific device
607
            $group = '';
608
            $redirectUrl = 0;
609
            $redirects = [];
610
            foreach ($redirect as $index => $oneRedirect) {
611
                if ($oneRedirect["device"] == $deviceIndex) {
612
                    $redirects[] = $oneRedirect;
613
                }
614
            }
615
            if (count($redirects) > 0) {
616
                $redirectUrl = $this->languageInstance->getLocalisedValue($redirects);
617
            }
618
            $devStatus = self::AVAILABLE;
619
            $message = 0;
620
            if (isset($deviceProperties['options']) && isset($deviceProperties['options']['message']) && $deviceProperties['options']['message']) {
621
                $message = $deviceProperties['options']['message'];
622
            }
623
            if (isset($deviceProperties['group'])) {
624
                $group = $deviceProperties['group'];
625
            }
626
            $eapCustomtext = 0;
627
            $deviceCustomtext = 0;
628
            if ($redirectUrl === 0) {
629
                if (isset($deviceProperties['options']) && isset($deviceProperties['options']['redirect']) && $deviceProperties['options']['redirect']) {
630
                    $devStatus = self::HIDDEN;
631
                } else {
632
                    $dev->calculatePreferredEapType($preferredEap);
633
                    $eap = $dev->selectedEap;
634
                    if (count($eap) > 0) {
635
                        if (isset($eAPOptions["eap-specific:customtext"][serialize($eap)])) {
636
                            $eapCustomtext = $eAPOptions["eap-specific:customtext"][serialize($eap)];
637
                        } else {
638
                            // fetch customtexts from method-level attributes
639
                            $eapCustomtext = 0;
640
                            $customTextAttributes = [];
641
                            $attributeList = $this->getAttributes("eap-specific:customtext"); // eap-specific attributes always have the array index 'eapmethod' set
642
                            foreach ($attributeList as $oneAttribute) {
643
                                if ($oneAttribute["eapmethod"] == $eap) {
644
                                    $customTextAttributes[] = $oneAttribute;
645
                                }
646
                            }
647
                            if (count($customTextAttributes) > 0) {
648
                                $eapCustomtext = $this->languageInstance->getLocalisedValue($customTextAttributes);
649
                            }
650
                            $eAPOptions["eap-specific:customtext"][serialize($eap)] = $eapCustomtext;
651
                        }
652
                        // fetch customtexts for device
653
                        $customTextAttributes = [];
654
                        $attributeList = $this->getAttributes("device-specific:customtext");
655
                        foreach ($attributeList as $oneAttribute) {
656
                            if ($oneAttribute["device"] == $deviceIndex) { // device-specific attributes always have the array index "device" set
657
                                $customTextAttributes[] = $oneAttribute;
658
                            }
659
                        }
660
                        $deviceCustomtext = $this->languageInstance->getLocalisedValue($customTextAttributes);
661
                    } else {
662
                        $devStatus = self::UNAVAILABLE;
663
                    }
664
                }
665
            }
666
            $returnarray[] = ['id' => $deviceIndex, 'display' => $deviceProperties['display'], 'status' => $devStatus, 'redirect' => $redirectUrl, 'eap_customtext' => $eapCustomtext, 'device_customtext' => $deviceCustomtext, 'message' => $message, 'options' => $deviceProperties['options'], 'group' => $group];
667
        }
668
        return $returnarray;
669
    }
670
671
    /**
672
     * prepare profile attributes for device modules
673
     * Gets profile attributes taking into account the most specific level on which they may be defined
674
     * as wel as the chosen language.
675
     * can be called with an optional $eap argument
676
     * 
677
     * @param array $eap if specified, retrieves all attributes except those not pertaining to the given EAP type
678
     * @return array list of attributes in collapsed style (index is the attrib name, value is an array of different values)
679
     */
680
    public function getCollapsedAttributes($eap = [])
681
    {
682
        $collapsedList = [];
683
        foreach ($this->getAttributes() as $attribute) {
684
            // filter out eap-level attributes not pertaining to EAP type $eap
685
            if (count($eap) > 0 && isset($attribute['eapmethod']) && $attribute['eapmethod'] != 0 && $attribute['eapmethod'] != $eap) {
686
                continue;
687
            }
688
            // create new array indexed by attribute name
689
            
690
            if (isset($attribute['device'])) {
691
                $collapsedList[$attribute['name']][$attribute['device']][] = $attribute['value'];
692
            } else {
693
                $collapsedList[$attribute['name']][] = $attribute['value'];
694
            } 
695
            // and keep all language-variant names in a separate sub-array
696
            if ($attribute['flag'] == "ML") {
697
                $collapsedList[$attribute['name']]['langs'][$attribute['lang']] = $attribute['value'];
698
            }
699
        }
700
        // once we have the final list, populate the respective "best-match"
701
        // language to choose for the ML attributes
702
        foreach ($collapsedList as $attribName => $valueArray) {
703
            if (isset($valueArray['langs'])) { // we have at least one language-dependent name in this attribute
704
                // for printed names on screen:
705
                // assign to exact match language, fallback to "default" language, fallback to English, fallback to whatever comes first in the list
706
                $collapsedList[$attribName][0] = $valueArray['langs'][$this->languageInstance->getLang()] ?? $valueArray['langs']['C'] ?? $valueArray['langs']['en'] ?? array_shift($valueArray['langs']);
707
                // for names usable in filesystems (closer to good old ASCII...)
708
                // prefer English, otherwise the "default" language, otherwise the same that we got above
709
                $collapsedList[$attribName][1] = $valueArray['langs']['en'] ?? $valueArray['langs']['C'] ?? $collapsedList[$attribName][0];
710
            }
711
        }
712
        return $collapsedList;
713
    }
714
715
    const READINESS_LEVEL_NOTREADY = 0;
716
    const READINESS_LEVEL_SUFFICIENTCONFIG = 1;
717
    const READINESS_LEVEL_SHOWTIME = 2;
718
719
    /**
720
     * Does the profile contain enough information to generate installers with
721
     * it? Silverbullet will always return TRUE; RADIUS profiles need to do some
722
     * heavy lifting here.
723
     * 
724
     * @return int one of the constants above which tell if enough info is set to enable installers
725
     */
726
    public function readinessLevel()
727
    {
728
        $result = $this->databaseHandle->exec("SELECT sufficient_config, showtime FROM profile WHERE profile_id = ?", "i", $this->identifier);
729
        // SELECT queries always return a resource, not a boolean
730
        $configQuery = mysqli_fetch_row(/** @scrutinizer ignore-type */ $result);
731
        if ($configQuery[0] == "0") {
732
            return self::READINESS_LEVEL_NOTREADY;
733
        }
734
        // at least fully configured, if not showtime!
735
        if ($configQuery[1] == "0") {
736
            return self::READINESS_LEVEL_SUFFICIENTCONFIG;
737
        }
738
        return self::READINESS_LEVEL_SHOWTIME;
739
    }
740
741
    /**
742
     * Checks if the profile has enough information to have something to show to end users. This does not necessarily mean
743
     * that there's a fully configured EAP type - it is sufficient if a redirect has been set for at least one device.
744
     * 
745
     * @return boolean TRUE if enough information for showtime is set; FALSE if not
746
     */
747
    private function readyForShowtime()
748
    {
749
        $properConfig = FALSE;
750
        $attribs = $this->getCollapsedAttributes();
751
        // do we have enough to go live? Check if any of the configured EAP methods is completely configured ...
752
        if (sizeof($this->getEapMethodsinOrderOfPreference(1)) > 0) {
753
            $properConfig = TRUE;
754
        }
755
        // if not, it could still be that general redirect has been set
756
        if (!$properConfig) {
757
            if (isset($attribs['device-specific:redirect'])) {
758
                $properConfig = TRUE;
759
            }
760
            // just a per-device redirect? would be good enough... but this is not actually possible:
761
            // per-device redirects can only be set on the "fine-tuning" page, which is only accessible
762
            // if at least one EAP type is fully configured - which is caught above and makes readyForShowtime TRUE already
763
        }
764
        // do we know at least one SSID to configure, or work with wired? If not, it's not ready...
765
        if (!isset($attribs['media:SSID']) &&
766
                (!isset(\config\ConfAssistant::CONSORTIUM['ssid']) || count(\config\ConfAssistant::CONSORTIUM['ssid']) == 0) &&
767
                !isset($attribs['media:wired'])) {
768
            $properConfig = FALSE;
769
        }
770
        return $properConfig;
771
    }
772
773
    /**
774
     * set the showtime property if prepShowTime says that there is enough info *and* the admin flagged the profile for showing
775
     * 
776
     * @return void
777
     */
778
    public function prepShowtime()
779
    {
780
        $properConfig = $this->readyForShowtime();
781
        $this->databaseHandle->exec("UPDATE profile SET sufficient_config = " . ($properConfig ? "TRUE" : "FALSE") . " WHERE profile_id = " . $this->identifier);
782
783
        $attribs = $this->getCollapsedAttributes();
784
        // if not enough info to go live, set FALSE
785
        // even if enough info is there, admin has the ultimate say: 
786
        //   if he doesn't want to go live, no further checks are needed, set FALSE as well
787
        if (!$properConfig || !isset($attribs['profile:production']) || (isset($attribs['profile:production']) && $attribs['profile:production'][0] != "on")) {
788
            $this->databaseHandle->exec("UPDATE profile SET showtime = FALSE WHERE profile_id = ?", "i", $this->identifier);
789
            return;
790
        }
791
        $this->databaseHandle->exec("UPDATE profile SET showtime = TRUE WHERE profile_id = ?", "i", $this->identifier);
792
    }
793
794
    /**
795
     * internal helper - some attributes are added by the constructor "ex officio"
796
     * without actual input from the admin. We can streamline their addition in
797
     * this function to avoid duplication.
798
     * 
799
     * @param array $internalAttributes - only names and value
800
     * @return array full attributes with all properties set
801
     */
802
    protected function addInternalAttributes($internalAttributes)
803
    {
804
        // internal attributes share many attribute properties, so condense the generation
805
        $retArray = [];
806
        foreach ($internalAttributes as $attName => $attValue) {
807
            $retArray[] = ["name" => $attName,
808
                "lang" => NULL,
809
                "value" => $attValue,
810
                "level" => Options::LEVEL_PROFILE,
811
                "row" => 0,
812
                "flag" => NULL,
813
            ];
814
        }
815
        return $retArray;
816
    }
817
818
    /**
819
     * Retrieves profile attributes stored in the database
820
     * 
821
     * @return array The attributes in one array
822
     */
823
    protected function addDatabaseAttributes()
824
    {
825
        $databaseAttributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row
826
                FROM $this->entityOptionTable
827
                WHERE $this->entityIdColumn = ?
828
                AND device_id IS NULL AND eap_method_id = 0
829
                ORDER BY option_name", "Profile");
830
        return $databaseAttributes;
831
    }
832
}