Passed
Push — master ( 5c87ed...e4bb53 )
by Stefan
04:05
created

AbstractProfile   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 655
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 113
dl 0
loc 655
rs 1.4052
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A saveDownloadDetails() 0 8 3
B levelPrecedenceAttributeJoin() 0 13 6
B __construct() 0 31 4
A fetchEAPMethods() 0 13 2
A getRealmCheckOuterUsername() 0 18 4
A updateFreshness() 0 2 1
A getFreshness() 0 5 2
A profileFromRealm() 0 9 2
B testCache() 0 16 5
A destroy() 0 4 1
B incrementDownloadStats() 0 19 7
C getUserDownloadStats() 0 26 7
A getEapMethodsinOrderOfPreference() 0 12 4
C isEapTypeDefinitionComplete() 0 32 12
A addSupportedEapMethod() 0 6 1
C listDevices() 0 74 21
A setRealm() 0 3 1
A addDatabaseAttributes() 0 7 1
A readinessLevel() 0 12 3
B prepShowtime() 0 13 6
D getCollapsedAttributes() 0 28 9
C readyForShowtime() 0 24 9
A addInternalAttributes() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractProfile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractProfile, and based on these observations, apply Extract Interface, too.

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
/**
13
 * This file contains the AbstractProfile class. It contains common methods for
14
 * both RADIUS/EAP profiles and SilverBullet profiles
15
 *
16
 * @author Stefan Winter <[email protected]>
17
 * @author Tomasz Wolniewicz <[email protected]>
18
 *
19
 * @package Developer
20
 *
21
 */
22
23
namespace core;
24
25
use \Exception;
26
27
/**
28
 * This class represents an EAP Profile.
29
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
30
 * one can set attribute in the Profile directly. If there is a conflict between
31
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
32
 * 
33
 * @author Stefan Winter <[email protected]>
34
 * @author Tomasz Wolniewicz <[email protected]>
35
 *
36
 * @license see LICENSE file in root directory
37
 *
38
 * @package Developer
39
 */
40
abstract class AbstractProfile extends EntityWithDBProperties {
41
42
    const HIDDEN = -1;
43
    const AVAILABLE = 0;
44
    const UNAVAILABLE = 1;
45
    const INCOMPLETE = 2;
46
    const NOTCONFIGURED = 3;
47
48
    /**
49
     * DB identifier of the parent institution of this profile
50
     * @var int
51
     */
52
    public $institution;
53
54
    /**
55
     * name of the parent institution of this profile in the current language
56
     * @var string
57
     */
58
    public $instName;
59
60
    /**
61
     * realm of this profile (empty string if unset)
62
     * @var string
63
     */
64
    public $realm;
65
66
    /**
67
     * This array holds the supported EAP types (in object representation). 
68
     * 
69
     * They are not synced against the DB after instantiation.
70
     * 
71
     * @var array
72
     */
73
    protected $privEaptypes;
74
75
    /**
76
     * number of profiles of the IdP this profile is attached to
77
     */
78
    protected $idpNumberOfProfiles;
79
80
    /**
81
     * IdP-wide attributes of the IdP this profile is attached to
82
     */
83
    protected $idpAttributes;
84
85
    /**
86
     * Federation level attributes that this profile is attached to via its IdP
87
     */
88
    protected $fedAttributes;
89
90
    /**
91
     * This class also needs to handle frontend operations, so needs its own
92
     * access to the FRONTEND datbase. This member stores the corresponding 
93
     * handle.
94
     * 
95
     * @var DBConnection
96
     */
97
    protected $frontendHandle;
98
99
    protected function saveDownloadDetails($idpIdentifier, $profileId, $deviceId, $area, $lang, $eapType) {
100
        if (CONFIG['PATHS']['logdir']) {
0 ignored issues
show
Bug introduced by
The constant core\CONFIG was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
101
            $file = fopen(CONFIG['PATHS']['logdir'] . "/download_details.log", "a");
102
            if ($file === FALSE) {
103
                throw new Exception("Unable to open file for append: $file");
104
            }
105
            fprintf($file, "%-015s;%d;%d;%s;%s;%s;%d\n", microtime(TRUE), $idpIdentifier, $profileId, $deviceId, $area, $lang, $eapType);
106
            fclose($file);
107
        }
108
    }
109
110
    /**
111
     * each profile has supported EAP methods, so get this from DB, Silver Bullet has one
112
     * static EAP method.
113
     */
114
    protected function fetchEAPMethods() {
115
        $eapMethod = $this->databaseHandle->exec("SELECT eap_method_id 
116
                                                        FROM supported_eap supp 
117
                                                        WHERE supp.profile_id = $this->identifier 
118
                                                        ORDER by preference");
119
        $eapTypeArray = [];
120
        // SELECTs never return a boolean, it's always a resource
121
        while ($eapQuery = (mysqli_fetch_object(/** @scrutinizer ignore-type */ $eapMethod))) {
122
            $eaptype = new common\EAP($eapQuery->eap_method_id);
123
            $eapTypeArray[] = $eaptype;
124
        }
125
        $this->loggerInstance->debug(4, "This profile supports the following EAP types:\n" . print_r($eapTypeArray, true));
126
        return $eapTypeArray;
127
    }
128
129
    /**
130
     * Class constructor for existing profiles (use IdP::newProfile() to actually create one). Retrieves all attributes and 
131
     * supported EAP types from the DB and stores them in the priv_ arrays.
132
     * 
133
     * sub-classes need to set the property $realm, $name themselves!
134
     * 
135
     * @param int $profileIdRaw identifier of the profile in the DB
136
     * @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.
137
     */
138
    public function __construct($profileIdRaw, $idpObject = NULL) {
139
        $this->databaseType = "INST";
140
        parent::__construct(); // we now have access to our INST database handle and logging
141
        $this->frontendHandle = DBConnection::handle("FRONTEND");
142
        // first make sure that we are operating on numeric identifiers
143
        if (!is_numeric($profileIdRaw)) {
144
            throw new Exception("Non-numeric Profile identifier was passed to AbstractProfile constructor!");
145
        }
146
        $profileId = (int) $profileIdRaw; // no, it can not possibly be a double. Try to convince Scrutinizer...
147
        $profile = $this->databaseHandle->exec("SELECT inst_id FROM profile WHERE profile_id = $profileId");
148
        // SELECT always yields a resource, never a boolean
149
        if ($profile->num_rows == 0) {
150
            $this->loggerInstance->debug(2, "Profile $profileId not found in database!\n");
151
            throw new Exception("Profile $profileId not found in database!");
152
        }
153
        $this->identifier = $profileId;
154
        $profileQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $profile);
155
        if (!($idpObject instanceof IdP)) {
156
            $this->institution = $profileQuery->inst_id;
157
            $idp = new IdP($this->institution);
158
        } else {
159
            $idp = $idpObject;
160
            $this->institution = (int) $idp->identifier;
161
        }
162
163
        $this->instName = $idp->name;
164
        $this->idpNumberOfProfiles = $idp->profileCount();
165
        $this->idpAttributes = $idp->getAttributes();
166
        $fedObject = new Federation($idp->federation);
167
        $this->fedAttributes = $fedObject->getAttributes();
168
        $this->loggerInstance->debug(3, "--- END Constructing new AbstractProfile object ... ---\n");
169
    }
170
171
    /**
172
     * join new attributes to existing ones, but only if not already defined on
173
     * a different level in the existing set
174
     * @param array $existing the already existing attributes
175
     * @param array $new the new set of attributes
176
     * @param string $newlevel the level of the new attributes
177
     * @return array the new set of attributes
178
     */
179
    protected function levelPrecedenceAttributeJoin($existing, $new, $newlevel) {
180
        foreach ($new as $attrib) {
181
            $ignore = "";
182
            foreach ($existing as $approvedAttrib) {
183
                if ($attrib["name"] == $approvedAttrib["name"] && $approvedAttrib["level"] != $newlevel) {
184
                    $ignore = "YES";
185
                }
186
            }
187
            if ($ignore != "YES") {
188
                $existing[] = $attrib;
189
            }
190
        }
191
        return $existing;
192
    }
193
194
    /**
195
     * find a profile, given its realm
196
     */
197
    public static function profileFromRealm($realm) {
198
        // static, need to create our own handle
199
        $handle = DBConnection::handle("INST");
200
        $execQuery = $handle->exec("SELECT profile_id FROM profile WHERE realm LIKE '%@$realm'");
201
        // a SELECT query always yields a resource, not a boolean
202
        if ($profileIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execQuery)) {
203
            return $profileIdQuery->profile_id;
204
        }
205
        return FALSE;
206
    }
207
208
    /**
209
     * Constructs the outer ID which should be used during realm tests. Obviously
210
     * can only do something useful if the realm is known to the system.
211
     * 
212
     * @return string the outer ID to use for realm check operations
213
     * @thorws Exception
214
     */
215
    public function getRealmCheckOuterUsername() {
216
        $realm = $this->getAttributes("internal:realm")[0]['value'] ?? FALSE;
217
        if ($realm == FALSE) { // we can't really return anything useful here
218
            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.");
219
        }
220
        if (count($this->getAttributes("internal:checkuser_outer")) > 0) {
221
            // we are supposed to use a specific outer username for checks, 
222
            // which is different from the outer username we put into installers
223
            return $this->getAttributes("internal:checkuser_value")[0]['value'] . "@" . $realm;
224
        }
225
        if (count($this->getAttributes("internal:use_anon_outer")) > 0) {
226
            // no special check username, but there is an anon outer ID for
227
            // installers - so let's use that one
228
            return $this->getAttributes("internal:anon_local_value")[0]['value'] . "@" . $realm;
229
        }
230
        // okay, no guidance on outer IDs at all - but we need *something* to
231
        // test with for the RealmChecks. So:
232
        return "@" . $realm;
233
    }
234
235
    /**
236
     * update the last_changed timestamp for this profile
237
     */
238
    public function updateFreshness() {
239
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE profile_id = $this->identifier");
240
    }
241
242
    /**
243
     * gets the last-modified timestamp (useful for caching "dirty" check)
244
     */
245
    public function getFreshness() {
246
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM profile WHERE profile_id = $this->identifier");
247
        // SELECT always returns a resource, never a boolean
248
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
249
            return $freshnessQuery->last_change;
250
        }
251
    }
252
253
    /**
254
     * tests if the configurator needs to be regenerated
255
     * returns the configurator path or NULL if regeneration is required
256
     */
257
    /**
258
     * This function tests if the configurator needs to be regenerated 
259
     * (properties of the Profile may have changed since the last configurator 
260
     * generation).
261
     * SilverBullet will always return NULL here because all installers are new!
262
     * 
263
     * @param string $device device ID to check
264
     * @return mixed a string with the path to the configurator download, or NULL if it needs to be regenerated
265
     */
266
267
    /**
268
     * This function tests if the configurator needs to be regenerated (properties of the Profile may have changed since the last configurator generation).
269
     * 
270
     * @param string $device device ID to check
271
     * @return mixed a string with the path to the configurator download, or NULL if it needs to be regenerated
272
     */
273
    public function testCache($device) {
274
        $returnValue = NULL;
275
        $lang = $this->languageInstance->getLang();
276
        $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);
277
        // SELECT queries always return a resource, not a boolean
278
        if ($result && $cache = mysqli_fetch_object(/** @scrutinizer ignore-type */ $result)) {
279
            $execUpdate = $this->databaseHandle->exec("SELECT UNIX_TIMESTAMP(last_change) AS last_change FROM profile WHERE profile_id = $this->identifier");
280
            // SELECT queries always return a resource, not a boolean
281
            if ($lastChange = /** @scrutinizer ignore-type */ mysqli_fetch_object($execUpdate)->last_change) {
0 ignored issues
show
Bug introduced by
It seems like $execUpdate can also be of type boolean; however, parameter $result of mysqli_fetch_object() does only seem to accept mysqli_result, 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

281
            if ($lastChange = /** @scrutinizer ignore-type */ mysqli_fetch_object(/** @scrutinizer ignore-type */ $execUpdate)->last_change) {
Loading history...
282
                if ($lastChange < $cache->tm) {
283
                    $this->loggerInstance->debug(4, "Installer cached:$cache->download_path\n");
284
                    $returnValue = ['cache' => $cache->download_path, 'mime' => $cache->mime];
285
                }
286
            }
287
        }
288
        return $returnValue;
289
    }
290
291
    /**
292
     * Updates database with new installer location. Actually does stuff when
293
     * caching is possible; is a noop if not
294
     * 
295
     * @param string $device the device identifier string
296
     * @param string $path the path where the new installer can be found
297
     * @param string $mime the mime type of the new installer
298
     * @param int $integerEapType the inter-representation of the EAP type that is configured in this installer
299
     */
300
    abstract public function updateCache($device, $path, $mime, $integerEapType);
301
302
    /**
303
     * Log a new download for our stats
304
     * 
305
     * @param string $device the device id string
306
     * @param string $area either admin or user
307
     * @return boolean TRUE if incrementing worked, FALSE if not
308
     */
309
    public function incrementDownloadStats($device, $area) {
310
        if ($area == "admin" || $area == "user" || $area == "silverbullet") {
311
            $lang = $this->languageInstance->getLang();
312
            $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);
313
            // get eap_type from the downloads table
314
            $eapTypeQuery = $this->frontendHandle->exec("SELECT eap_type FROM downloads WHERE profile_id = ? AND device_id = ? AND lang = ?", "iss", $this->identifier, $device, $lang);
315
            // SELECT queries always return a resource, not a boolean
316
            if (!$eapTypeQuery || !$eapO = mysqli_fetch_object(/** @scrutinizer ignore-type */ $eapTypeQuery)) {
317
                $this->loggerInstance->debug(2, "Error getting EAP_type from the database\n");
318
            } else {
319
                if ($eapO->eap_type == NULL) {
320
                    $this->loggerInstance->debug(2, "EAP_type not set in the database\n");
321
                } else {
322
                    $this->saveDownloadDetails($this->institution, $this->identifier, $device, $area, $this->languageInstance->getLang(), $eapO->eap_type);
323
                }
324
            }
325
            return TRUE;
326
        }
327
        return FALSE;
328
    }
329
330
    /**
331
     * Retrieve current download stats from database, either for one specific device or for all devices
332
     * @param string $device the device id string
333
     * @return mixed user downloads of this profile; if device is given, returns the counter as int, otherwise an array with devicename => counter
334
     */
335
    public function getUserDownloadStats($device = NULL) {
336
        $columnName = "downloads_user";
337
        if ($this instanceof \core\ProfileSilverbullet) {
338
            $columnName = "downloads_silverbullet";
339
        }
340
        $returnarray = [];
341
        $numbers = $this->frontendHandle->exec("SELECT device_id, SUM($columnName) AS downloads_user FROM downloads WHERE profile_id = ? GROUP BY device_id", "i", $this->identifier);
342
        // SELECT queries always return a resource, not a boolean
343
        while ($statsQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $numbers)) {
344
            $returnarray[$statsQuery->device_id] = $statsQuery->downloads_user;
345
        }
346
        if ($device !== NULL) {
347
            if (isset($returnarray[$device])) {
348
                return $returnarray[$device];
349
            }
350
            return 0;
351
        }
352
        // we should pretty-print the device names
353
        $finalarray = [];
354
        $devlist = \devices\Devices::listDevices();
355
        foreach ($returnarray as $devId => $count) {
356
            if (isset($devlist[$devId])) {
357
                $finalarray[$devlist[$devId]['display']] = $count;
358
            }
359
        }
360
        return $finalarray;
361
    }
362
363
    /**
364
     * Deletes the profile from database and uninstantiates itself.
365
     * Works fine also for Silver Bullet; the first query will simply do nothing
366
     * because there are no stored options
367
     *
368
     */
369
    public function destroy() {
370
        $this->databaseHandle->exec("DELETE FROM profile_option WHERE profile_id = $this->identifier");
371
        $this->databaseHandle->exec("DELETE FROM supported_eap WHERE profile_id = $this->identifier");
372
        $this->databaseHandle->exec("DELETE FROM profile WHERE profile_id = $this->identifier");
373
    }
374
375
    /**
376
     * Specifies the realm of this profile.
377
     * 
378
     * Forcefully type-hinting $realm parameter to string - Scrutinizer seems to
379
     * think that it can alternatively be an array<integer,?> which looks like a
380
     * false positive. If there really is an issue, let PHP complain about it at
381
     * runtime.
382
     * 
383
     * @param string $realm the realm (potentially with the local@ part that should be used for anonymous identities)
384
     */
385
    public function setRealm(string $realm) {
386
        $this->databaseHandle->exec("UPDATE profile SET realm = ? WHERE profile_id = ?", "si", $realm, $this->identifier);
387
        $this->realm = $realm;
0 ignored issues
show
Documentation Bug introduced by
It seems like $realm of type array is incompatible with the declared type string of property $realm.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
388
    }
389
390
    /**
391
     * register new supported EAP method for this profile
392
     *
393
     * @param \core\common\EAP $type The EAP Type, as defined in class EAP
394
     * @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.
395
     *
396
     */
397
    public function addSupportedEapMethod(\core\common\EAP $type, $preference) {
398
        $this->databaseHandle->exec("INSERT INTO supported_eap (profile_id, eap_method_id, preference) VALUES ("
399
                . $this->identifier . ", "
400
                . $type->getIntegerRep() . ", "
401
                . $preference . ")");
402
        $this->updateFreshness();
403
    }
404
405
    /**
406
     * Produces an array of EAP methods supported by this profile, ordered by preference
407
     * 
408
     * @param int $completeOnly if set and non-zero limits the output to methods with complete information
409
     * @return array list of EAP methods, (in object representation)
410
     */
411
    public function getEapMethodsinOrderOfPreference($completeOnly = 0) {
412
        $temparray = [];
413
414
        if ($completeOnly == 0) {
415
            return $this->privEaptypes;
416
        }
417
        foreach ($this->privEaptypes as $type) {
418
            if ($this->isEapTypeDefinitionComplete($type) === true) {
419
                $temparray[] = $type;
420
            }
421
        }
422
        return($temparray);
423
    }
424
425
    /**
426
     * Performs a sanity check for a given EAP type - did the admin submit enough information to create installers for him?
427
     * 
428
     * @param common\EAP $eaptype the EAP type
429
     * @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
430
     */
431
    public function isEapTypeDefinitionComplete($eaptype) {
432
        if ($eaptype->needsServerCACert() && $eaptype->needsServerName()) {
433
            $missing = [];
434
            // silverbullet needs a support email address configured
435
            if ($eaptype->getIntegerRep() == common\EAP::INTEGER_SILVERBULLET && count($this->getAttributes("support:email")) == 0) {
436
                return ["support:email"];
437
            }
438
            $cnOption = $this->getAttributes("eap:server_name"); // cannot be set per device or eap type
439
            $caOption = $this->getAttributes("eap:ca_file"); // cannot be set per device or eap type
440
441
            if (count($caOption) > 0 && count($cnOption) > 0) {// see if we have at least one root CA cert
442
                foreach ($caOption as $oneCa) {
443
                    $x509 = new \core\common\X509();
444
                    $caParsed = $x509->processCertificate($oneCa['value']);
445
                    if ($caParsed['root'] == 1) {
446
                        return TRUE;
447
                    }
448
                }
449
                $missing[] = "eap:ca_file";
450
            }
451
            if (count($caOption) == 0) {
452
                $missing[] = "eap:ca_file";
453
            }
454
            if (count($cnOption) == 0) {
455
                $missing[] = "eap:server_name";
456
            }
457
            if (count($missing) == 0) {
458
                return TRUE;
459
            }
460
            return $missing;
461
        }
462
        return TRUE;
463
    }
464
465
    /**
466
     * list all devices marking their availabiblity and possible redirects
467
     *
468
     * @return array of device ids display names and their status
469
     */
470
    public function listDevices() {
471
        $returnarray = [];
472
        $redirect = $this->getAttributes("device-specific:redirect"); // this might return per-device ones or the general one
473
        // if it was a general one, we are done. Find out if there is one such
474
        // which has device = NULL
475
        $generalRedirect = NULL;
476
        foreach ($redirect as $index => $oneRedirect) {
477
            if ($oneRedirect["level"] == "Profile") {
478
                $generalRedirect = $index;
479
            }
480
        }
481
        if ($generalRedirect !== NULL) { // could be index 0
482
            return [['id' => '0', 'redirect' => $redirect[$generalRedirect]['value']]];
483
        }
484
        $preferredEap = $this->getEapMethodsinOrderOfPreference(1);
485
        $eAPOptions = [];
486
        foreach (\devices\Devices::listDevices() as $deviceIndex => $deviceProperties) {
487
            $factory = new DeviceFactory($deviceIndex);
488
            $dev = $factory->device;
489
            // find the attribute pertaining to the specific device
490
            $redirectUrl = 0;
491
            foreach ($redirect as $index => $oneRedirect) {
492
                if ($oneRedirect["device"] == $deviceIndex) {
493
                    $redirectUrl = $this->languageInstance->getLocalisedValue($oneRedirect);
494
                }
495
            }
496
            $devStatus = self::AVAILABLE;
497
            $message = 0;
498
            if (isset($deviceProperties['options']) && isset($deviceProperties['options']['message']) && $deviceProperties['options']['message']) {
499
                $message = $deviceProperties['options']['message'];
500
            }
501
            $eapCustomtext = 0;
502
            $deviceCustomtext = 0;
503
            if ($redirectUrl === 0) {
504
                if (isset($deviceProperties['options']) && isset($deviceProperties['options']['redirect']) && $deviceProperties['options']['redirect']) {
505
                    $devStatus = self::HIDDEN;
506
                } else {
507
                    $dev->calculatePreferredEapType($preferredEap);
508
                    $eap = $dev->selectedEap;
509
                    if (count($eap) > 0) {
510
                        if (isset($eAPOptions["eap-specific:customtext"][serialize($eap)])) {
511
                            $eapCustomtext = $eAPOptions["eap-specific:customtext"][serialize($eap)];
512
                        } else {
513
                            // fetch customtexts from method-level attributes
514
                            $eapCustomtext = 0;
515
                            $customTextAttributes = [];
516
                            $attributeList = $this->getAttributes("eap-specific:redirect"); // eap-specific attributes always have the array index 'eapmethod' set
517
                            foreach ($attributeList as $oneAttribute) {
518
                                if ($oneAttribute["eapmethod"] == $eap) {
519
                                    $customTextAttributes[] = $oneAttribute;
520
                                }
521
                            }
522
                            if (count($customTextAttributes) > 0) {
523
                                $eapCustomtext = $this->languageInstance->getLocalisedValue($customTextAttributes);
524
                            }
525
                            $eAPOptions["eap-specific:customtext"][serialize($eap)] = $eapCustomtext;
526
                        }
527
                        // fetch customtexts for device
528
                        $customTextAttributes = [];
529
                        $attributeList = $this->getAttributes("device-specific:redirect");
530
                        foreach ($attributeList as $oneAttribute) {
531
                            if ($oneAttribute["device"] == $deviceIndex) { // device-specific attributes always have the array index "device" set
532
                                $customTextAttributes[] = $oneAttribute;
533
                            }
534
                        }
535
                        $deviceCustomtext = $this->languageInstance->getLocalisedValue($customTextAttributes);
536
                    } else {
537
                        $devStatus = self::UNAVAILABLE;
538
                    }
539
                }
540
            }
541
            $returnarray[] = ['id' => $deviceIndex, 'display' => $deviceProperties['display'], 'status' => $devStatus, 'redirect' => $redirectUrl, 'eap_customtext' => $eapCustomtext, 'device_customtext' => $deviceCustomtext, 'message' => $message, 'options' => $deviceProperties['options']];
542
        }
543
        return $returnarray;
544
    }
545
546
    /**
547
     * prepare profile attributes for device modules
548
     * Gets profile attributes taking into account the most specific level on which they may be defined
549
     * as wel as the chosen language.
550
     * can be called with an optional $eap argument
551
     * 
552
     * @param array $eap if specified, retrieves all attributes except those not pertaining to the given EAP type
553
     * @return array list of attributes in collapsed style (index is the attrib name, value is an array of different values)
554
     */
555
    public function getCollapsedAttributes($eap = []) {
556
        $collapsedList = [];
557
        foreach ($this->getAttributes() as $attribute) {
558
            // filter out eap-level attributes not pertaining to EAP type $eap
559
            if (count($eap) > 0 && isset($attrib['eapmethod']) && $attrib['eapmethod'] != 0 && $attrib['eapmethod'] != $eap) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $attrib seems to never exist and therefore isset should always be false.
Loading history...
560
                continue;
561
            }
562
            // create new array indexed by attribute name
563
            $collapsedList[$attribute['name']][] = $attribute['value'];
564
            // and keep all language-variant names in a separate sub-array
565
            if ($attribute['flag'] == "ML") {
566
                $collapsedList[$attribute['name']]['langs'][$attribute['lang']] = $attribute['value'];
567
            }
568
        }
569
        // once we have the final list, populate the respective "best-match"
570
        // language to choose for the ML attributes
571
        foreach ($collapsedList as $attribName => $valueArray) {
572
            if (isset($valueArray['langs'])) { // we have at least one language-dependent name in this attribute
573
                // for printed names on screen:
574
                // assign to exact match language, fallback to "default" language, fallback to English, fallback to whatever comes first in the list
575
                $collapsedList[$attribName][0] = $valueArray['langs'][$this->languageInstance->getLang()] ?? $valueArray['langs']['C'] ?? $valueArray['langs']['en'] ?? array_shift($valueArray['langs']);
576
                // for names usable in filesystems (closer to good old ASCII...)
577
                // prefer English, otherwise the "default" language, otherwise the same that we got above
578
                $collapsedList[$attribName][1] = $valueArray['langs']['en'] ?? $valueArray['langs']['C'] ?? $collapsedList[$attribName][0];
579
            }
580
        }
581
582
        return $collapsedList;
583
    }
584
585
    const READINESS_LEVEL_NOTREADY = 0;
586
    const READINESS_LEVEL_SUFFICIENTCONFIG = 1;
587
    const READINESS_LEVEL_SHOWTIME = 2;
588
589
    /**
590
     * Does the profile contain enough information to generate installers with
591
     * it? Silverbullet will always return TRUE; RADIUS profiles need to do some
592
     * heavy lifting here.
593
     * 
594
     * * @return int one of the constants above which tell if enough info is set to enable installers
595
     */
596
    public function readinessLevel() {
597
        $result = $this->databaseHandle->exec("SELECT sufficient_config, showtime FROM profile WHERE profile_id = ?", "i", $this->identifier);
598
        // SELECT queries always return a resource, not a boolean
599
        $configQuery = mysqli_fetch_row(/** @scrutinizer ignore-type */ $result);
600
        if ($configQuery[0] == "0") {
601
            return self::READINESS_LEVEL_NOTREADY;
602
        }
603
        // at least fully configured, if not showtime!
604
        if ($configQuery[1] == "0") {
605
            return self::READINESS_LEVEL_SUFFICIENTCONFIG;
606
        }
607
        return self::READINESS_LEVEL_SHOWTIME;
608
    }
609
610
    /**
611
     * Checks if the profile has enough information to have something to show to end users. This does not necessarily mean
612
     * that there's a fully configured EAP type - it is sufficient if a redirect has been set for at least one device.
613
     * 
614
     * @return boolean TRUE if enough information for showtime is set; FALSE if not
615
     */
616
    private function readyForShowtime() {
617
        $properConfig = FALSE;
618
        $attribs = $this->getCollapsedAttributes();
619
        // do we have enough to go live? Check if any of the configured EAP methods is completely configured ...
620
        if (sizeof($this->getEapMethodsinOrderOfPreference(1)) > 0) {
621
            $properConfig = TRUE;
622
        }
623
        // if not, it could still be that general redirect has been set
624
        if (!$properConfig) {
625
            if (isset($attribs['device-specific:redirect'])) {
626
                $properConfig = TRUE;
627
            }
628
            // just a per-device redirect? would be good enough... but this is not actually possible:
629
            // per-device redirects can only be set on the "fine-tuning" page, which is only accessible
630
            // if at least one EAP type is fully configured - which is caught above and makes readyForShowtime TRUE already
631
        }
632
        // do we know at least one SSID to configure, or work with wired? If not, it's not ready...
633
        if (!isset($attribs['media:SSID']) &&
634
                !isset($attribs['media:SSID_with_legacy']) &&
635
                (!isset(CONFIG_CONFASSISTANT['CONSORTIUM']['ssid']) || count(CONFIG_CONFASSISTANT['CONSORTIUM']['ssid']) == 0) &&
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
636
                !isset($attribs['media:wired'])) {
637
            $properConfig = FALSE;
638
        }
639
        return $properConfig;
640
    }
641
642
    /**
643
     * set the showtime property if prepShowTime says that there is enough info *and* the admin flagged the profile for showing
644
     */
645
    public function prepShowtime() {
646
        $properConfig = $this->readyForShowtime();
647
        $this->databaseHandle->exec("UPDATE profile SET sufficient_config = " . ($properConfig ? "TRUE" : "FALSE") . " WHERE profile_id = " . $this->identifier);
648
649
        $attribs = $this->getCollapsedAttributes();
650
        // if not enough info to go live, set FALSE
651
        // even if enough info is there, admin has the ultimate say: 
652
        //   if he doesn't want to go live, no further checks are needed, set FALSE as well
653
        if (!$properConfig || !isset($attribs['profile:production']) || (isset($attribs['profile:production']) && $attribs['profile:production'][0] != "on")) {
654
            $this->databaseHandle->exec("UPDATE profile SET showtime = FALSE WHERE profile_id = ?", "i", $this->identifier);
655
            return;
656
        }
657
        $this->databaseHandle->exec("UPDATE profile SET showtime = TRUE WHERE profile_id = ?", "i", $this->identifier);
658
    }
659
660
    /**
661
     * internal helper - some attributes are added by the constructor "ex officio"
662
     * without actual input from the admin. We can streamline their addition in
663
     * this function to avoid duplication.
664
     * 
665
     * @param array $internalAttributes - only names and value
666
     * @return array full attributes with all properties set
667
     */
668
    protected function addInternalAttributes($internalAttributes) {
669
        // internal attributes share many attribute properties, so condense the generation
670
        $retArray = [];
671
        foreach ($internalAttributes as $attName => $attValue) {
672
            $retArray[] = ["name" => $attName,
673
                "lang" => NULL,
674
                "value" => $attValue,
675
                "level" => "Profile",
676
                "row" => 0,
677
                "flag" => NULL,
678
            ];
679
        }
680
        return $retArray;
681
    }
682
683
    /**
684
     * Retrieves profile attributes stored in the database
685
     * 
686
     * @return array The attributes in one array
687
     */
688
    protected function addDatabaseAttributes() {
689
        $databaseAttributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row
690
                FROM $this->entityOptionTable
691
                WHERE $this->entityIdColumn = ?
692
                AND device_id IS NULL AND eap_method_id = 0
693
                ORDER BY option_name", "Profile");
694
        return $databaseAttributes;
695
    }
696
697
}
698