Passed
Push — master ( 4302eb...519f93 )
by Stefan
11:45
created

AbstractProfile::addDatabaseAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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