Passed
Push — master ( 28eddd...1e0b05 )
by Stefan
08:22
created

AbstractProfile::prepShowtime()   B

Complexity

Conditions 6
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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