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

getEapMethodsinOrderOfPreference()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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