AbstractProfile   F
last analyzed

Complexity

Total Complexity 157

Size/Duplication

Total Lines 1022
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 157
eloc 461
c 5
b 0
f 0
dl 0
loc 1022
rs 2

27 Methods

Rating   Name   Duplication   Size   Complexity  
A testCache() 0 17 5
B significantChanges() 0 42 10
F openroamingRedinessTest() 0 117 22
A __construct() 0 32 4
A fetchEAPMethods() 0 14 2
A setOpenRoamingReadinessInfo() 0 3 1
A getRealmCheckOuterUsername() 0 19 4
A saveDownloadDetails() 0 9 3
A updateFreshness() 0 3 1
A getFreshness() 0 6 2
A profileFromRealm() 0 10 2
A addDatabaseAttributes() 0 8 1
A isRedirected() 0 6 2
A destroy() 0 5 1
A readinessLevel() 0 13 3
A prepShowtime() 0 14 6
B incrementDownloadStats() 0 21 7
A getEapMethodsinOrderOfPreference() 0 13 4
B getCollapsedAttributes() 0 34 10
C isEapTypeDefinitionComplete() 0 33 12
A certificateStatus() 0 20 4
A addSupportedEapMethod() 0 5 1
F listDevices() 0 96 28
B getUserDownloadStats() 0 44 10
B readyForShowtime() 0 28 9
A addInternalAttributes() 0 14 2
A setRealm() 0 4 1

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
/**
24
 * This file contains the AbstractProfile class. It contains common methods for
25
 * both RADIUS/EAP profiles and SilverBullet profiles
26
 *
27
 * @author Stefan Winter <[email protected]>
28
 * @author Tomasz Wolniewicz <[email protected]>
29
 *
30
 * @package Developer
31
 *
32
 */
33
34
namespace core;
35
36
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
38
/**
39
 * This class represents an EAP Profile.
40
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
41
 * one can set attribute in the Profile directly. If there is a conflict between
42
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
43
 * 
44
 * @author Stefan Winter <[email protected]>
45
 * @author Tomasz Wolniewicz <[email protected]>
46
 *
47
 * @license see LICENSE file in root directory
48
 *
49
 * @package Developer
50
 */
51
abstract class AbstractProfile extends EntityWithDBProperties
52
{
53
54
    const HIDDEN = -1;
55
    const AVAILABLE = 0;
56
    const UNAVAILABLE = 1;
57
    const INCOMPLETE = 2;
58
    const NOTCONFIGURED = 3;
59
    const PROFILETYPE_RADIUS = "RADIUS";
60
    const PROFILETYPE_SILVERBULLET = "SILVERBULLET";
61
    public const SERVERNAME_ADDED = 2;
62
    public const CA_ADDED = 3;
63
    public const CA_CLASH_ADDED = 4;
64
    public const SERVER_CERT_ADDED = 5;
65
    public const CA_ROOT_NO_EXT = 6;
66
67
    /**
68
     * DB identifier of the parent institution of this profile
69
     * @var integer
70
     */
71
    public $institution;
72
73
    /**
74
     * name of the parent institution of this profile in the current language
75
     * @var string
76
     */
77
    public $instName;
78
79
    /**
80
     * realm of this profile (empty string if unset)
81
     * @var string
82
     */
83
    public $realm;
84
85
    /**
86
     * This array holds the supported EAP types (in object representation). 
87
     * 
88
     * They are not synced against the DB after instantiation.
89
     * 
90
     * @var array
91
     */
92
    protected $privEaptypes;
93
94
    /**
95
     * number of profiles of the IdP this profile is attached to
96
     * 
97
     * @var integer
98
     */
99
    protected $idpNumberOfProfiles;
100
101
    /**
102
     * IdP-wide attributes of the IdP this profile is attached to
103
     * 
104
     * @var array
105
     */
106
    protected $idpAttributes;
107
108
    /**
109
     * Federation level attributes that this profile is attached to via its IdP
110
     * 
111
     * @var array
112
     */
113
    protected $fedAttributes;
114
115
    /**
116
     * This class also needs to handle frontend operations, so needs its own
117
     * access to the FRONTEND database. This member stores the corresponding 
118
     * handle.
119
     * 
120
     * @var DBConnection
121
     */
122
    protected $frontendHandle;
123
124
    /**
125
     * readiness levels for OpenRoaming column in profiles)
126
     */
127
    const OVERALL_OPENROAMING_LEVEL_NO = 4;
128
    const OVERALL_OPENROAMING_LEVEL_GOOD = 3;
129
    const OVERALL_OPENROAMING_LEVEL_NOTE = 2;
130
    const OVERALL_OPENROAMING_LEVEL_WARN = 1;
131
    const OVERALL_OPENROAMING_LEVEL_ERROR = 0;
132
    
133
/**
134
 * constants used for displaying messages
135
 */    
136
    const OPENROAMING_ALL_GOOD = 24;
137
    const OPENROAMING_NO_REALM = 17; //none
138
    const OPENROAMING_BAD_SRV = 16; //none
139
    const OPENROAMING_BAD_NAPTR = 10; // warning
140
    const OPENROAMING_SOME_BAD_CONNECTIONS = 8; //warning
141
    const OPENROAMING_NO_DNSSEC = 8; //warning
142
    const OPENROAMING_NO_NAPTR = 3; //error
143
    const OPENROAMING_BAD_NAPTR_RESOLVE = 2; //error
144
    const OPENROAMING_BAD_SRV_RESOLVE = 1; //error
145
    const OPENROAMING_BAD_CONNECTION = 0; //error
146
    
147
    
148
    const READINESS_LEVEL_NOTREADY = 0;
149
    const READINESS_LEVEL_SUFFICIENTCONFIG = 1;
150
    const READINESS_LEVEL_SHOWTIME = 2;
151
    
152
    
153
    const CERT_STATUS_NONE = -1;
154
    const CERT_STATUS_OK = 0;
155
    const CERT_STATUS_WARN = 1;
156
    const CERT_STATUS_ERROR = 2;
157
    
158
    const OVERALL_OPENROAMING_INDEX = [
159
        self::OVERALL_OPENROAMING_LEVEL_NO => 'OVERALL_OPENROAMING_LEVEL_NO',
160
        self::OVERALL_OPENROAMING_LEVEL_GOOD => 'OVERALL_OPENROAMING_LEVEL_GOOD',
161
        self::OVERALL_OPENROAMING_LEVEL_NOTE => 'OVERALL_OPENROAMING_LEVEL_NOTE',
162
        self::OVERALL_OPENROAMING_LEVEL_WARN => 'OVERALL_OPENROAMING_LEVEL_WARN',
163
        self::OVERALL_OPENROAMING_LEVEL_ERROR => 'OVERALL_OPENROAMING_LEVEL_ERROR',      
164
    ];
165
166
    const OPENROAMING_INDEX = [
167
        self::OVERALL_OPENROAMING_LEVEL_NO => 'OVERALL_OPENROAMING_LEVEL_NO',
168
    ];
169
    
170
    const CERT_STATUS_INDEX = [
171
        self::CERT_STATUS_OK => 'CERT_STATUS_OK',
172
        self::CERT_STATUS_WARN => 'CERT_STATUS_WARN',
173
        self::CERT_STATUS_ERROR => 'CERT_STATUS_ERROR',     
174
    ];
175
    
176
    /**
177
     *  generates a detailed log of which installer was downloaded
178
     * 
179
     * @param int    $idpIdentifier the IdP identifier
180
     * @param int    $profileId     the Profile identifier
181
     * @param string $deviceId      the Device identifier
182
     * @param string $area          the download area (user, silverbullet, admin)
183
     * @param string $lang          the language of the installer
184
     * @param int    $eapType       the EAP type of the installer
185
     * @return void
186
     * @throws Exception
187
     */
188
    protected function saveDownloadDetails($idpIdentifier, $profileId, $deviceId, $area, $lang, $eapType, $openRoaming)
189
    {
190
        if (\config\Master::PATHS['logdir']) {
191
            $file = fopen(\config\Master::PATHS['logdir']."/download_details.log", "a");
192
            if ($file === FALSE) {
193
                throw new Exception("Unable to open file for append: $file");
194
            }
195
            fprintf($file, "%-015s;%d;%d;%s;%s;%s;%d;%d\n", microtime(TRUE), $idpIdentifier, $profileId, $deviceId, $area, $lang, $eapType, $openRoaming);
196
            fclose($file);
197
        }
198
    }
199
200
    /**
201
     * checks if security-relevant parameters have changed
202
     * 
203
     * @param AbstractProfile $old old instantiation of a profile to compare against
204
     * @param AbstractProfile $new new instantiation of a profile 
205
     * @return array there are never any user-induced changes in SB
206
     */
207
    public static function significantChanges($old, $new)
208
    {
209
        $retval = [];
210
        // check if a CA was added
211
        $x509 = new common\X509();
212
        $baselineCA = [];
213
        $baselineCApublicKey = [];
214
        foreach ($old->getAttributes("eap:ca_file") as $oldCA) {
215
            $ca = $x509->processCertificate($oldCA['value']);
216
            $baselineCA[$ca['sha1']] = $ca['name'];
217
            $baselineCApublicKey[$ca['sha1']] = $ca['full_details']['public_key'];
218
        }
219
        // remove the new ones that are identical to the baseline
220
        foreach ($new->getAttributes("eap:ca_file") as $newCA) {
221
            $ca = $x509->processCertificate($newCA['value']);
222
            if (array_key_exists($ca['sha1'], $baselineCA) || $ca['root'] != 1) {
223
                // do nothing; we assume here that SHA1 doesn't clash
224
                continue;
225
            }
226
            // check if a CA with identical DN was added - alert NRO if so
227
            $foundSHA1 = array_search($ca['name'], $baselineCA);
228
            if ($foundSHA1 !== FALSE) {
229
                // but only if the public key does not match
230
                if ($baselineCApublicKey[$foundSHA1] === $ca['full_details']['public_key']) {
231
                    continue;
232
                }
233
                $retval[AbstractProfile::CA_CLASH_ADDED] .= "#SHA1 for CA with DN '".$ca['name']."' has SHA1 fingerprints (pre-existing) "./** @scrutinizer ignore-type */ array_search($ca['name'], $baselineCA)." and (added) ".$ca['sha1'];
234
            } else {
235
                $retval[AbstractProfile::CA_ADDED] .= "#CA with DN '"./** @scrutinizer ignore-type */ print_r($ca['name'], TRUE)."' and SHA1 fingerprint ".$ca['sha1']." was added as trust anchor";
236
            }
237
        }
238
        // check if a servername was added
239
        $baselineNames = [];
240
        foreach ($old->getAttributes("eap:server_name") as $oldName) {
241
            $baselineNames[] = $oldName['value'];
242
        }
243
        foreach ($new->getAttributes("eap:server_name") as $newName) {
244
            if (!in_array($newName['value'], $baselineNames)) {
245
                $retval[AbstractProfile::SERVERNAME_ADDED] .= "#New server name '".$newName['value']."' added";
246
            }
247
        }
248
        return $retval;
249
    }
250
251
    /**
252
     * Tests OpenRoaming aspects of the profile like DNS settings and server reachibility
253
     * 
254
     * @return array of arrays of the form [['level' => $level, 'explanation' => $explanation, 'reason' => $reason]];
255
     */
256
    public function openroamingRedinessTest() {
257
        // do OpenRoaming initial diagnostic checks
258
        // numbers correspond to RFC7585Tests::OVERALL_LEVEL
259
        $results = [];
260
        $resultLevel = $this::OVERALL_OPENROAMING_LEVEL_GOOD; // assume all is well, degrade if we have concrete findings to suggest otherwise
261
        $tag = "aaa+auth:radius.tls.tcp";
262
        // do we know the realm at all? Notice if not.
263
        if (!isset($this->getAttributes("internal:realm")[0]['value'])) {
264
            $explanation = _("The profile information does not include the realm, so no DNS checks for OpenRoaming can be executed.");
265
            $level = $this::OVERALL_OPENROAMING_LEVEL_NOTE;
266
            $reason = $this::OPENROAMING_NO_REALM;
267
            $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
268
            $resultLevel = min([$resultLevel, $level]);
269
        } else {
270
            $dnsChecks = new \core\diag\RFC7585Tests($this->getAttributes("internal:realm")[0]['value'], $tag);
271
            $relevantNaptrRecords = $dnsChecks->relevantNAPTR();
272
            if ($relevantNaptrRecords <= 0) {
273
                $explanation = _("There is no relevant DNS NAPTR record ($tag) for this realm. OpenRoaming will not work.");
274
                $reason = $this::OPENROAMING_NO_NAPTR;
275
                $level = $this::OVERALL_OPENROAMING_LEVEL_ERROR;
276
                $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
277
                $resultLevel = min([$resultLevel, $level]);
278
            } else {
279
                $recordCompliance = $dnsChecks->relevantNAPTRcompliance();
280
                if ($recordCompliance != \core\diag\AbstractTest::RETVAL_OK) {
281
                    $explanation = _("The DNS NAPTR record ($tag) for this realm is not syntax conform. OpenRoaming will likely not work.");
282
                    $reason = $this::OPENROAMING_BAD_NAPTR;
283
                    $level = $this::OVERALL_OPENROAMING_LEVEL_WARN;
284
                    $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
285
                    $resultLevel = min([$resultLevel, $level]);
286
                }
287
                // check if target is the expected one, if set by NRO
288
                foreach ($this->fedAttributes as $attr) {
289
                    if ($attr['name'] === 'fed:openroaming_customtarget') {
290
                        $customText = $attr['value'];
291
                    } else {
292
                        $customText = '';
293
                    }
294
                }
295
                if ($customText !== '') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $customText seems to be defined by a foreach iteration on line 288. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
296
                    foreach ($dnsChecks->NAPTR_records as $orpointer) {
297
                        if ($orpointer["replacement"] != $customText) {
298
                            $explanation = _("The SRV target of an OpenRoaming NAPTR record is unexpected.");
299
                            $reason = $this::OPENROAMING_BAD_SRV;
300
                            $level = $this::OVERALL_OPENROAMING_LEVEL_NOTE;
301
                            $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
302
                            $resultLevel = min([$resultLevel, $level]);
303
                        }
304
                    }
305
                }
306
                $srvResolution = $dnsChecks->relevantNAPTRsrvResolution();
307
                $hostnameResolution = $dnsChecks->relevantNAPTRhostnameResolution();
308
309
                if ($srvResolution <= 0) {
310
                    $explanation = _("The DNS SRV target for NAPTR $tag does not resolve. OpenRoaming will not work.");
311
                    $level = $this::OVERALL_OPENROAMING_LEVEL_ERROR;
312
                    $reason = $this::OPENROAMING_BAD_NAPTR_RESOLVE;
313
                    $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
314
                    $resultLevel = min([$resultLevel, $this::OVERALL_OPENROAMING_LEVEL_ERROR]);
315
                } elseif ($hostnameResolution <= 0) {
316
                    $explanation = _("The DNS hostnames in the SRV records do not resolve to actual host IPs. OpenRoaming will not work.");
317
                    $level = $this::OVERALL_OPENROAMING_LEVEL_ERROR;
318
                    $reason = $this::OPENROAMING_BAD_SRV_RESOLVE;
319
                    $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
320
                    $resultLevel = min([$resultLevel, $level]);
321
                }
322
                // connect to all IPs we found and see if they are really an OpenRoaming server
323
                $allHostsOkay = TRUE;
324
                $oneHostOkay = FALSE;
325
                $testCandidates = [];
326
                foreach ($dnsChecks->NAPTR_hostname_records as $oneServer) {
327
                    $testCandidates[$oneServer['hostname']][] = ($oneServer['family'] == "IPv4" ? $oneServer['IP'] : "[".$oneServer['IP']."]").":".$oneServer['port'];
328
                }
329
                foreach ($testCandidates as $oneHost => $listOfIPs) {
330
                    $connectionTests = new \core\diag\RFC6614Tests(array_values($listOfIPs), $oneHost, "openroaming");
331
                    // for now (no OpenRoaming client certs available) only run server-side tests
332
                    foreach ($listOfIPs as $oneIP) {
333
                        $connectionResult = $connectionTests->cApathCheck($oneIP);
334
                        if ($connectionResult != \core\diag\AbstractTest::RETVAL_OK || ( isset($connectionTests->TLS_CA_checks_result['cert_oddity']) && count($connectionTests->TLS_CA_checks_result['cert_oddity']) > 0)) {
335
                            $allHostsOkay = FALSE;
336
                        } else {
337
                            $oneHostOkay = TRUE;
338
                        }
339
                    }
340
                }
341
                if (!$allHostsOkay) {
342
                    if (!$oneHostOkay) {
343
                        $explanation = _("When connecting to the discovered OpenRoaming endpoints, they all had errors. OpenRoaming will likely not work.");
344
                        $level = $this::OVERALL_OPENROAMING_LEVEL_ERROR;
345
                        $reason = $this::OPENROAMING_BAD_CONNECTION;
346
                        $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
347
                        $resultLevel = min([$resultLevel, $level]);
348
                    } else {
349
                        $explanation = _("When connecting to the discovered OpenRoaming endpoints, only a subset of endpoints had no errors.");
350
                        $level = $this::OVERALL_OPENROAMING_LEVEL_WARN;
351
                        $reason = $this::OPENROAMING_SOME_BAD_CONNECTIONS;
352
                        $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
353
                        $resultLevel = min([$resultLevel, $level]);
354
                    }
355
                }
356
            }
357
        }
358
        if (!$dnsChecks->allResponsesSecure) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $dnsChecks does not seem to be defined for all execution paths leading up to this point.
Loading history...
359
            $explanation = _("At least one DNS response was NOT secured using DNSSEC. OpenRoaming ANPs may refuse to connect to the endpoint.");
360
            $level = $this::OVERALL_OPENROAMING_LEVEL_WARN;
361
            $reason = $this::OPENROAMING_NO_DNSSEC;
362
            $results[] = ['level' => $level, 'explanation' => $explanation, 'reason' => $reason];
363
            $resultLevel = min([$resultLevel, $level]);
364
        }
365
        if ($resultLevel == $this::OVERALL_OPENROAMING_LEVEL_GOOD) {
366
            $explanation = _("Initial diagnostics regarding the DNS part of OpenRoaming (including DNSSEC) were successful.");
367
            $level = $this::OVERALL_OPENROAMING_LEVEL_GOOD;
368
            $reason = $this::OPENROAMING_ALL_GOOD;
369
            $results = [['level' => $level, 'explanation' => $explanation, 'reason' => $reason]];
370
        }               
371
        $this->setOpenRoamingReadinessInfo($resultLevel);
372
        return $results;
373
    }
374
    
375
    /**
376
     * Takes note of the OpenRoaming participation and conformance level
377
     * 
378
     * @param int $level the readiness level, as determined by RFC7585Tests
379
     * @return void
380
     */
381
    public function setOpenRoamingReadinessInfo(int $level)
382
    {
383
            $this->databaseHandle->exec("UPDATE profile SET openroaming = ? WHERE profile_id = ?", "ii", $level, $this->identifier);
384
    }
385
386
    /**
387
     * each profile has supported EAP methods, so get this from DB, Silver Bullet has one
388
     * static EAP method.
389
     * 
390
     * @return array list of supported EAP methods
391
     */
392
    protected function fetchEAPMethods()
393
    {
394
        $eapMethod = $this->databaseHandle->exec("SELECT eap_method_id 
395
                                                        FROM supported_eap supp 
396
                                                        WHERE supp.profile_id = $this->identifier 
397
                                                        ORDER by preference");
398
        $eapTypeArray = [];
399
        // SELECTs never return a boolean, it's always a resource
400
        while ($eapQuery = (mysqli_fetch_object(/** @scrutinizer ignore-type */ $eapMethod))) {
401
            $eaptype = new common\EAP($eapQuery->eap_method_id);
402
            $eapTypeArray[] = $eaptype;
403
        }
404
        $this->loggerInstance->debug(4, "This profile supports the following EAP types:\n"./** @scrutinizer ignore-type */ print_r($eapTypeArray, true));
405
        return $eapTypeArray;
406
    }
407
408
    /**
409
     * Class constructor for existing profiles (use IdP::newProfile() to actually create one). Retrieves all attributes and 
410
     * supported EAP types from the DB and stores them in the priv_ arrays.
411
     * 
412
     * sub-classes need to set the property $realm, $name themselves!
413
     * 
414
     * @param int $profileIdRaw identifier of the profile in the DB
415
     * @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.
416
     * @throws Exception
417
     */
418
    public function __construct($profileIdRaw, $idpObject = NULL)
419
    {
420
        $this->databaseType = "INST";
421
        parent::__construct(); // we now have access to our INST database handle and logging
422
        $handle = DBConnection::handle("FRONTEND");
423
        if ($handle instanceof DBConnection) {
424
            $this->frontendHandle = $handle;
425
        } else {
426
            throw new Exception("This database type is never an array!");
427
        }
428
        $profile = $this->databaseHandle->exec("SELECT inst_id FROM profile WHERE profile_id = ?", "i", $profileIdRaw);
429
        // SELECT always yields a resource, never a boolean
430
        if ($profile->num_rows == 0) {
431
            $this->loggerInstance->debug(2, "Profile $profileIdRaw not found in database!\n");
432
            throw new Exception("Profile $profileIdRaw not found in database!");
433
        }
434
        $this->identifier = $profileIdRaw;
435
        $profileQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $profile);
436
        if (!($idpObject instanceof IdP)) {
437
            $this->institution = $profileQuery->inst_id;
438
            $idp = new IdP($this->institution);
439
        } else {
440
            $idp = $idpObject;
441
            $this->institution = $idp->identifier;
442
        }
443
444
        $this->instName = $idp->name;
445
        $this->idpNumberOfProfiles = $idp->profileCount();
446
        $this->idpAttributes = $idp->getAttributes();
447
        $fedObject = new Federation($idp->federation);
448
        $this->fedAttributes = $fedObject->getAttributes();
449
        $this->loggerInstance->debug(4, "--- END Constructing new AbstractProfile object ... ---\n");
450
    }
451
452
    /**
453
     * find a profile, given its realm
454
     * 
455
     * @param string $realm the realm for which we are trying to find a profile
456
     * @return int|false the profile identifier, if any
457
     */
458
    public static function profileFromRealm($realm)
459
    {
460
        // static, need to create our own handle
461
        $handle = DBConnection::handle("INST");
462
        $execQuery = $handle->exec("SELECT profile_id FROM profile WHERE realm LIKE '%@$realm'");
463
        // a SELECT query always yields a resource, not a boolean
464
        if ($profileIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execQuery)) {
465
            return $profileIdQuery->profile_id;
466
        }
467
        return FALSE;
468
    }
469
470
    /**
471
     * Constructs the outer ID which should be used during realm tests. Obviously
472
     * can only do something useful if the realm is known to the system.
473
     * 
474
     * @return string the outer ID to use for realm check operations
475
     * @throws Exception
476
     */
477
    public function getRealmCheckOuterUsername()
478
    {
479
        $realm = $this->getAttributes("internal:realm")[0]['value'] ?? FALSE;
480
        if ($realm == FALSE) { // we can't really return anything useful here
481
            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.");
482
        }
483
        if (count($this->getAttributes("internal:checkuser_outer")) > 0) {
484
            // we are supposed to use a specific outer username for checks, 
485
            // which is different from the outer username we put into installers
486
            return $this->getAttributes("internal:checkuser_value")[0]['value']."@".$realm;
487
        }
488
        if (count($this->getAttributes("internal:use_anon_outer")) > 0) {
489
            // no special check username, but there is an anon outer ID for
490
            // installers - so let's use that one
491
            return $this->getAttributes("internal:anon_local_value")[0]['value']."@".$realm;
492
        }
493
        // okay, no guidance on outer IDs at all - but we need *something* to
494
        // test with for the RealmChecks. So:
495
        return "@".$realm;
496
    }
497
498
    /**
499
     * update the last_changed timestamp for this profile
500
     * 
501
     * @return void
502
     */
503
    public function updateFreshness()
504
    {
505
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE profile_id = $this->identifier");
506
    }
507
508
    /**
509
     * gets the last-modified timestamp (useful for caching "dirty" check)
510
     * 
511
     * @return string the date in string form, as returned by SQL
512
     */
513
    public function getFreshness()
514
    {
515
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM profile WHERE profile_id = $this->identifier");
516
        // SELECT always returns a resource, never a boolean
517
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
518
            return $freshnessQuery->last_change;
519
        }
520
    }
521
    /**
522
     * tests if the configurator needs to be regenerated
523
     * returns the configurator path or NULL if regeneration is required
524
     */
525
    /**
526
     * This function tests if the configurator needs to be regenerated 
527
     * (properties of the Profile may have changed since the last configurator 
528
     * generation).
529
     * SilverBullet will always return NULL here because all installers are new!
530
     * 
531
     * @param string $device device ID to check
532
     * @return mixed a string with the path to the configurator download, or NULL if it needs to be regenerated
533
     */
534
535
    /**
536
     * This function tests if the configurator needs to be regenerated (properties of the Profile may have changed since the last configurator generation).
537
     * 
538
     * @param string $device device ID to check
539
     * @return mixed a string with the path to the configurator download, or NULL if it needs to be regenerated
540
     */
541
    public function testCache($device, $openRoaming)
542
    {
543
        $returnValue = ['cache' => NULL, 'mime' => NULL];
544
        $lang = $this->languageInstance->getLang();
545
        $result = $this->frontendHandle->exec("SELECT download_path, mime, UNIX_TIMESTAMP(installer_time) AS tm FROM downloads WHERE profile_id = ? AND device_id = ? AND lang = ? AND openroaming = ?", "issi", $this->identifier, $device, $lang, $openRoaming);
546
        // SELECT queries always return a resource, not a boolean
547
        if ($result && $cache = mysqli_fetch_object(/** @scrutinizer ignore-type */ $result)) {
548
            $execUpdate = $this->databaseHandle->exec("SELECT UNIX_TIMESTAMP(last_change) AS last_change FROM profile WHERE profile_id = $this->identifier");
549
            // SELECT queries always return a resource, not a boolean
550
            if ($lastChange = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execUpdate)->last_change) {
551
                if ($lastChange < $cache->tm) {
552
                    $this->loggerInstance->debug(4, "Installer cached:$cache->download_path\n");
553
                    $returnValue = ['cache' => $cache->download_path, 'mime' => $cache->mime];
554
                }
555
            }
556
        }
557
        return $returnValue;
558
    }
559
560
    /**
561
     * Updates database with new installer location. Actually does stuff when
562
     * caching is possible; is a noop if not
563
     * 
564
     * @param string $device         the device identifier string
565
     * @param string $path           the path where the new installer can be found
566
     * @param string $mime           the mime type of the new installer
567
     * @param int    $integerEapType the inter-representation of the EAP type that is configured in this installer
568
     * @return void
569
     */
570
    abstract public function updateCache($device, $path, $mime, $integerEapType, $openRoaming);
571
572
    /** Toggle anonymous outer ID support.
573
     *
574
     * @param boolean $shallwe TRUE to enable outer identities (needs valid $realm), FALSE to disable
575
     * @return void
576
     */
577
    abstract public function setAnonymousIDSupport($shallwe);
578
579
    /**
580
     * Log a new download for our stats
581
     * 
582
     * @param string $device the device id string
583
     * @param string $area   either admin or user
584
     * @return boolean TRUE if incrementing worked, FALSE if not
585
     */
586
    public function incrementDownloadStats($device, $area, $openRoaming)
587
    {
588
        if ($area == "admin" || $area == "user" || $area == "silverbullet") {
589
            $lang = $this->languageInstance->getLang();
590
            $this->frontendHandle->exec("INSERT INTO downloads (profile_id, device_id, lang, openroaming, downloads_$area) VALUES (? ,?, ?, ?, 1) ON DUPLICATE KEY UPDATE downloads_$area = downloads_$area + 1", "issi", $this->identifier, $device, $lang, $openRoaming);
591
            $this->frontendHandle->exec("INSERT INTO downloads_history (profile_id, device_id, downloads_$area, openroaming, stat_date) VALUES (?, ?, 1, ?, DATE_FORMAT(NOW(), '%Y-%m-01')) ON DUPLICATE KEY UPDATE downloads_$area = downloads_$area + 1", "isi", $this->identifier, $device, $openRoaming);
592
            // get eap_type from the downloads table
593
            $eapTypeQuery = $this->frontendHandle->exec("SELECT eap_type FROM downloads WHERE profile_id = ? AND device_id = ? AND lang = ?", "iss", $this->identifier, $device, $lang);
594
            // SELECT queries always return a resource, not a boolean
595
            if (!$eapTypeQuery || !$eapO = mysqli_fetch_object(/** @scrutinizer ignore-type */ $eapTypeQuery)) {
596
                $this->loggerInstance->debug(2, "Error getting EAP_type from the database\n");
597
            } else {
598
                if ($eapO->eap_type == NULL) {
599
                    $this->loggerInstance->debug(2, "EAP_type not set in the database\n");
600
                } else {
601
                    $this->saveDownloadDetails($this->institution, $this->identifier, $device, $area, $this->languageInstance->getLang(), $eapO->eap_type, $openRoaming);
602
                }
603
            }
604
            return TRUE;
605
        }
606
        return FALSE;
607
    }
608
609
    /**
610
     * Retrieve current download stats from database, either for one specific device or for all devices
611
     * 
612
     * @param string $device the device id string
613
     * @return mixed user downloads of this profile; if device is given, returns the counter as int, otherwise an array with devicename => counter
614
     */
615
    public function getUserDownloadStats($device = NULL)
616
    {
617
        $columnName = "downloads_user";
618
        if ($this instanceof \core\ProfileSilverbullet) {
619
            $columnName = "downloads_silverbullet";
620
        }
621
        $returnarray = [];
622
        $numbers = $this->frontendHandle->exec("SELECT device_id, SUM($columnName) AS downloads_user FROM downloads WHERE profile_id = ? GROUP BY device_id", "i", $this->identifier);
623
        // SELECT queries always return a resource, not a boolean
624
        while ($statsQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $numbers)) {
625
            $returnarray[$statsQuery->device_id] = $statsQuery->downloads_user;
626
        }
627
        if ($device !== NULL) {
628
            if (isset($returnarray[$device])) {
629
                return $returnarray[$device];
630
            }
631
            return 0;
632
        }
633
        // we should pretty-print the device names
634
        $finalarray = [];
635
        $devlist = \devices\Devices::listDevices($this->identifier);
636
        foreach ($returnarray as $devId => $count) {
637
            if (isset($devlist[$devId])) {
638
                $finalarray[$devlist[$devId]['display']]['current'] = $count;
639
            }
640
            
641
        }
642
643
        $monthlyList = [];
644
        $monthly = $this->frontendHandle->exec("SELECT downloads_user,device_id FROM downloads_history WHERE profile_id=? AND stat_date=DATE_FORMAT(NOW(),'%Y-%m-01')", "i",  $this->identifier);
645
        while ($statsQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $monthly)) {
646
            $monthlyList[$statsQuery->device_id] = $statsQuery->downloads_user;
647
        }
648
        foreach ($monthlyList as $devId => $count) {
649
            if (isset($devlist[$devId])) {
650
                $finalarray[$devlist[$devId]['display']]['monthly'] = $count;
651
            }
652
            
653
        }        
654
        
655
        \core\common\Entity::intoThePotatoes();
656
        ksort($finalarray, SORT_STRING|SORT_FLAG_CASE);
657
        \core\common\Entity::outOfThePotatoes();
658
        return $finalarray;
659
    }
660
661
    /**
662
     * Deletes the profile from database and uninstantiates itself.
663
     * Works fine also for Silver Bullet; the first query will simply do nothing
664
     * because there are no stored options
665
     * 
666
     * @return void
667
     */
668
    public function destroy()
669
    {
670
        $this->databaseHandle->exec("DELETE FROM profile_option WHERE profile_id = $this->identifier");
671
        $this->databaseHandle->exec("DELETE FROM supported_eap WHERE profile_id = $this->identifier");
672
        $this->databaseHandle->exec("DELETE FROM profile WHERE profile_id = $this->identifier");
673
    }
674
675
    /**
676
     * Specifies the realm of this profile.
677
     * 
678
     * Forcefully type-hinting $realm parameter to string - Scrutinizer seems to
679
     * think that it can alternatively be an array<integer,?> which looks like a
680
     * false positive. If there really is an issue, let PHP complain about it at
681
     * runtime.
682
     * 
683
     * @param string $realm the realm (potentially with the local@ part that should be used for anonymous identities)
684
     * @return void
685
     */
686
    public function setRealm(string $realm)
687
    {
688
        $this->databaseHandle->exec("UPDATE profile SET realm = ? WHERE profile_id = ?", "si", $realm, $this->identifier);
689
        $this->realm = $realm;
690
    }
691
692
    /**
693
     * register new supported EAP method for this profile
694
     *
695
     * @param \core\common\EAP $type       The EAP Type, as defined in class EAP
696
     * @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.
697
     * @return void
698
     */
699
    public function addSupportedEapMethod(\core\common\EAP $type, $preference)
700
    {
701
        $eapInt = $type->getIntegerRep();
702
        $this->databaseHandle->exec("INSERT INTO supported_eap (profile_id, eap_method_id, preference) VALUES (?, ?, ?)", "iii", $this->identifier, $eapInt, $preference);
703
        $this->updateFreshness();
704
    }
705
706
    /**
707
     * Produces an array of EAP methods supported by this profile, ordered by preference
708
     * 
709
     * @param int $completeOnly if set and non-zero limits the output to methods with complete information
710
     * @return array list of EAP methods, (in object representation)
711
     */
712
    public function getEapMethodsinOrderOfPreference($completeOnly = 0)
713
    {
714
        $temparray = [];
715
716
        if ($completeOnly == 0) {
717
            return $this->privEaptypes;
718
        }
719
        foreach ($this->privEaptypes as $type) {
720
            if ($this->isEapTypeDefinitionComplete($type) === true) {
721
                $temparray[] = $type;
722
            }
723
        }
724
        return($temparray);
725
    }
726
727
    /**
728
     * Performs a sanity check for a given EAP type - did the admin submit enough information to create installers for him?
729
     * 
730
     * @param common\EAP $eaptype the EAP type
731
     * @return mixed TRUE if the EAP type is complete; an array of missing attributes if it's incomplete; FALSE if it's incomplete for other reasons
732
     */
733
    public function isEapTypeDefinitionComplete($eaptype)
734
    {
735
        if ($eaptype->needsServerCACert() && $eaptype->needsServerName()) {
736
            $missing = [];
737
            // silverbullet needs a support email address configured
738
            if ($eaptype->getIntegerRep() == common\EAP::INTEGER_SILVERBULLET && count($this->getAttributes("support:email")) == 0) {
739
                return ["support:email"];
740
            }
741
            $cnOption = $this->getAttributes("eap:server_name"); // cannot be set per device or eap type
742
            $caOption = $this->getAttributes("eap:ca_file"); // cannot be set per device or eap type
743
744
            if (count($caOption) > 0 && count($cnOption) > 0) {// see if we have at least one root CA cert
745
                foreach ($caOption as $oneCa) {
746
                    $x509 = new \core\common\X509();
747
                    $caParsed = $x509->processCertificate($oneCa['value']);
748
                    if ($caParsed['root'] == 1) {
749
                        return TRUE;
750
                    }
751
                }
752
                $missing[] = "eap:ca_file";
753
            }
754
            if (count($caOption) == 0) {
755
                $missing[] = "eap:ca_file";
756
            }
757
            if (count($cnOption) == 0) {
758
                $missing[] = "eap:server_name";
759
            }
760
            if (count($missing) == 0) {
761
                return TRUE;
762
            }
763
            return $missing;
764
        }
765
        return TRUE;
766
    }
767
768
    /**
769
     * list all devices marking their availabiblity and possible redirects
770
     *
771
     * @return array of device ids display names and their status
772
     */
773
    public function listDevices()
774
    {
775
        $returnarray = [];
776
        $redirect = $this->getAttributes("device-specific:redirect"); // this might return per-device ones or the general one
777
        // if it was a general one, we are done. Find out if there is one such
778
        // which has device = NULL
779
        $generalRedirect = NULL;
780
        foreach ($redirect as $index => $oneRedirect) {
781
            if ($oneRedirect["level"] == Options::LEVEL_PROFILE) {
782
                $generalRedirect = $index;
783
            }
784
        }
785
        if ($generalRedirect !== NULL) { // could be index 0
786
            return [['id' => '0', 'redirect' => $redirect[$generalRedirect]['value']]];
787
        }
788
        $preferredEap = $this->getEapMethodsinOrderOfPreference(1);
789
        $eAPOptions = [];
790
        if (count($this->getAttributes("media:openroaming")) == 1 && $this->getAttributes("media:openroaming")[0]['value'] == 'always-preagreed') {
791
            $orAlways = 1;
792
        } else {
793
            $orAlways = 0;
794
        }
795
        
796
        foreach (\devices\Devices::listDevices($this->identifier, $orAlways) as $deviceIndex => $deviceProperties) {
797
            $factory = new DeviceFactory($deviceIndex);
798
            $dev = $factory->device;
799
            // find the attribute pertaining to the specific device
800
            $group = '';
801
            $redirectUrl = 0;
802
            $redirects = [];
803
            foreach ($redirect as $index => $oneRedirect) {
804
                if ($oneRedirect["device"] == $deviceIndex) {
805
                    $redirects[] = $oneRedirect;
806
                }
807
            }
808
            if (count($redirects) > 0) {
809
                $redirectUrl = $this->languageInstance->getLocalisedValue($redirects);
810
            }
811
            $devStatus = self::AVAILABLE;
812
            $message = 0;
813
            if (isset($deviceProperties['options']) && isset($deviceProperties['options']['message']) && $deviceProperties['options']['message']) {
814
                $message = $deviceProperties['options']['message'];
815
            }
816
            if (isset($deviceProperties['group'])) {
817
                $group = $deviceProperties['group'];
818
            }
819
            $eapCustomtext = 0;
820
            $deviceCustomtext = 0;
821
            $geteduroam = 0;
822
            if ($redirectUrl === 0) {
823
                if (isset($deviceProperties['options']) && isset($deviceProperties['options']['redirect']) && $deviceProperties['options']['redirect']) {
824
                    $devStatus = self::HIDDEN;
825
                } else {
826
                    $dev->calculatePreferredEapType($preferredEap);
827
                    $eap = $dev->selectedEap;
828
                    if (count($eap) > 0) {
829
                        if (isset($eAPOptions["eap-specific:customtext"][serialize($eap)])) {
830
                            $eapCustomtext = $eAPOptions["eap-specific:customtext"][serialize($eap)];
831
                        } else {
832
                            // fetch customtexts from method-level attributes
833
                            $eapCustomtext = 0;
834
                            $customTextAttributes = [];
835
                            $attributeList = $this->getAttributes("eap-specific:customtext"); // eap-specific attributes always have the array index 'eapmethod' set
836
                            foreach ($attributeList as $oneAttribute) {
837
                                if ($oneAttribute["eapmethod"] == $eap) {
838
                                    $customTextAttributes[] = $oneAttribute;
839
                                }
840
                            }
841
                            if (count($customTextAttributes) > 0) {
842
                                $eapCustomtext = $this->languageInstance->getLocalisedValue($customTextAttributes);
843
                            }
844
                            $eAPOptions["eap-specific:customtext"][serialize($eap)] = $eapCustomtext;
845
                        }
846
                        // fetch customtexts for device
847
                        $customTextAttributes = [];
848
                        $attributeList = $this->getAttributes("device-specific:customtext");
849
                        foreach ($attributeList as $oneAttribute) {
850
                            if ($oneAttribute["device"] == $deviceIndex) { // device-specific attributes always have the array index "device" set
851
                                $customTextAttributes[] = $oneAttribute;
852
                            }
853
                        }
854
                        $deviceCustomtext = $this->languageInstance->getLocalisedValue($customTextAttributes);
855
                    } else {
856
                        $devStatus = self::UNAVAILABLE;
857
                    }
858
                    $geteduroamOpts = $this->getAttributes("device-specific:geteduroam");
859
                    foreach ($geteduroamOpts as $dev) {
860
                        if ($dev['device'] == $deviceIndex) {
861
                            $geteduroam = $dev['value'] == 'on' ? 1 : 0;
862
                        }
863
                    }
864
                }
865
            }
866
            $returnarray[] = ['id' => $deviceIndex, 'display' => $deviceProperties['display'], 'status' => $devStatus, 'redirect' => $redirectUrl, 'eap_customtext' => $eapCustomtext, 'device_customtext' => $deviceCustomtext, 'message' => $message, 'options' => $deviceProperties['options'], 'group' => $group, 'geteduroam' => $geteduroam];
867
        }
868
        return $returnarray;
869
    }
870
871
    /**
872
     * prepare profile attributes for device modules
873
     * Gets profile attributes taking into account the most specific level on which they may be defined
874
     * as well as the chosen language.
875
     * can be called with an optional $eap argument
876
     * 
877
     * @param array $eap if specified, retrieves all attributes except those not pertaining to the given EAP type
878
     * @return array list of attributes in collapsed style (index is the attrib name, value is an array of different values)
879
     */
880
    public function getCollapsedAttributes($eap = [])
881
    {
882
        $collapsedList = [];
883
        foreach ($this->getAttributes() as $attribute) {
884
            // filter out eap-level attributes not pertaining to EAP type $eap
885
            if (count($eap) > 0 && isset($attribute['eapmethod']) && $attribute['eapmethod'] != 0 && $attribute['eapmethod'] != $eap) {
886
                continue;
887
            }
888
            // create new array indexed by attribute name
889
            
890
            if (isset($attribute['device'])) {
891
                $collapsedList[$attribute['name']][$attribute['device']][] = $attribute['value'];
892
            } else {
893
                $collapsedList[$attribute['name']][] = $attribute['value'];
894
            } 
895
            // and keep all language-variant names in a separate sub-array
896
            if ($attribute['flag'] == "ML") {
897
                $collapsedList[$attribute['name']]['langs'][$attribute['lang']] = $attribute['value'];
898
            }
899
        }
900
        // once we have the final list, populate the respective "best-match"
901
        // language to choose for the ML attributes
902
903
        foreach ($collapsedList as $attribName => $valueArray) {
904
            if (isset($valueArray['langs'])) { // we have at least one language-dependent name in this attribute
905
                // for printed names on screen:
906
                // assign to exact match language, fallback to "default" language, fallback to English, fallback to whatever comes first in the list
907
                $collapsedList[$attribName][0] = $valueArray['langs'][$this->languageInstance->getLang()] ?? $valueArray['langs']['C'] ?? $valueArray['langs']['en'] ?? array_shift($valueArray['langs']);
908
                // for names usable in filesystems (closer to good old ASCII...)
909
                // prefer English, otherwise the "default" language, otherwise the same that we got above
910
                $collapsedList[$attribName][1] = $valueArray['langs']['en'] ?? $valueArray['langs']['C'] ?? $collapsedList[$attribName][0];
911
            }
912
        }
913
        return $collapsedList;
914
    }
915
    
916
    /**
917
     * Is the profile global redirection set?
918
     * 
919
     * @return bool
920
     */
921
    public function isRedirected() {
922
        $result = $this->databaseHandle->exec("SELECT profile_id FROM profile_option WHERE profile_id = ? AND option_name='device-specific:redirect' AND device_id IS NULL", "i", $this->identifier);
923
        if ($result->num_rows == 0) {
924
            return false;
925
        }
926
        return true;
927
    }
928
    
929
    /**
930
     * Does the profile contain enough information to generate installers with
931
     * it? Silverbullet will always return TRUE; RADIUS profiles need to do some
932
     * heavy lifting here.
933
     * 
934
     * @return int one of the constants above which tell if enough info is set to enable installers
935
     */
936
    public function readinessLevel()
937
    {
938
        $result = $this->databaseHandle->exec("SELECT sufficient_config, showtime FROM profile WHERE profile_id = ?", "i", $this->identifier);
939
        // SELECT queries always return a resource, not a boolean
940
        $configQuery = mysqli_fetch_row(/** @scrutinizer ignore-type */ $result);
941
        if ($configQuery[0] == "0") {
942
            return self::READINESS_LEVEL_NOTREADY;
943
        }
944
        // at least fully configured, if not showtime!
945
        if ($configQuery[1] == "0") {
946
            return self::READINESS_LEVEL_SUFFICIENTCONFIG;
947
        }
948
        return self::READINESS_LEVEL_SHOWTIME;
949
    }
950
951
    /**
952
     * Checks all profile certificates validity periods comparing to the pre-defined
953
     * thresholds and returns the most critical status.
954
     * 
955
     * @return int - one of constants defined in this profile
956
     */
957
    public function certificateStatus()
958
    {
959
        $query = "SELECT option_value AS cert FROM profile_option  WHERE option_name='eap:ca_file' AND profile_id = ?";        
960
        $result = $this->databaseHandle->exec($query, "i", $this->identifier);
961
        $rows = $result->fetch_all();
962
        $x509 = new \core\common\X509();
963
        $profileStatus = self::CERT_STATUS_NONE;
964
        foreach ($rows as $row) {
965
            $encodedCert = $row[0];
966
            $tm = $x509->processCertificate(base64_decode($encodedCert))['full_details']['validTo_time_t']- time();
967
            if ($tm < \config\ConfAssistant::CERT_WARNINGS['expiry_critical']) {
968
                $certStatus = self::CERT_STATUS_ERROR;
969
            } elseif ($tm < \config\ConfAssistant::CERT_WARNINGS['expiry_warning']) {
970
                $certStatus = self::CERT_STATUS_WARN;
971
            } else {
972
                $certStatus = self::CERT_STATUS_OK;
973
            }
974
            $profileStatus = max($profileStatus, $certStatus);
975
        }
976
        return $profileStatus;
977
    }
978
979
    /**
980
     * Checks if the profile has enough information to have something to show to end users. This does not necessarily mean
981
     * that there's a fully configured EAP type - it is sufficient if a redirect has been set for at least one device.
982
     * 
983
     * @return boolean TRUE if enough information for showtime is set; FALSE if not
984
     */
985
    private function readyForShowtime()
986
    {
987
        $properConfig = FALSE;
988
        $attribs = $this->getCollapsedAttributes();
989
        // do we have enough to go live? Check if any of the configured EAP methods is completely configured ...
990
        if (sizeof($this->getEapMethodsinOrderOfPreference(1)) > 0) {
991
            $properConfig = TRUE;
992
        }
993
        // if not, it could still be that general redirect has been set
994
        if (!$properConfig) {
995
            if (isset($attribs['device-specific:redirect'])) {
996
                $properConfig = TRUE;
997
            }
998
            // just a per-device redirect? would be good enough... but this is not actually possible:
999
            // per-device redirects can only be set on the "fine-tuning" page, which is only accessible
1000
            // if at least one EAP type is fully configured - which is caught above and makes readyForShowtime TRUE already
1001
        }
1002
        // do we know at least one SSID to configure, or work with wired? If not, it's not ready...
1003
        if (!isset($attribs['media:SSID']) &&
1004
                (!isset(\config\ConfAssistant::CONSORTIUM['ssid']) || count(\config\ConfAssistant::CONSORTIUM['ssid']) == 0) &&
1005
                !isset($attribs['media:wired'])) {
1006
            $properConfig = FALSE;
1007
        }
1008
        // institutions without a name are not really a corner case we should support
1009
        if (!isset($attribs['general:instname'])) {
1010
            $properConfig = FALSE;
1011
        }
1012
        return $properConfig;
1013
    }
1014
1015
    /**
1016
     * set the showtime property if prepShowTime says that there is enough info *and* the admin flagged the profile for showing
1017
     * 
1018
     * @return void
1019
     */
1020
    public function prepShowtime()
1021
    {
1022
        $properConfig = $this->readyForShowtime();
1023
        $this->databaseHandle->exec("UPDATE profile SET sufficient_config = ".($properConfig ? "TRUE" : "FALSE")." WHERE profile_id = ".$this->identifier);
1024
1025
        $attribs = $this->getCollapsedAttributes();
1026
        // if not enough info to go live, set FALSE
1027
        // even if enough info is there, admin has the ultimate say: 
1028
        //   if he doesn't want to go live, no further checks are needed, set FALSE as well
1029
        if (!$properConfig || !isset($attribs['profile:production']) || (isset($attribs['profile:production']) && $attribs['profile:production'][0] != "on")) {
1030
            $this->databaseHandle->exec("UPDATE profile SET showtime = FALSE WHERE profile_id = ?", "i", $this->identifier);
1031
            return;
1032
        }
1033
        $this->databaseHandle->exec("UPDATE profile SET showtime = TRUE WHERE profile_id = ?", "i", $this->identifier);
1034
    }
1035
1036
    /**
1037
     * internal helper - some attributes are added by the constructor "ex officio"
1038
     * without actual input from the admin. We can streamline their addition in
1039
     * this function to avoid duplication.
1040
     * 
1041
     * @param array $internalAttributes - only names and value
1042
     * @return array full attributes with all properties set
1043
     */
1044
    protected function addInternalAttributes($internalAttributes)
1045
    {
1046
        // internal attributes share many attribute properties, so condense the generation
1047
        $retArray = [];
1048
        foreach ($internalAttributes as $attName => $attValue) {
1049
            $retArray[] = ["name" => $attName,
1050
                "lang" => NULL,
1051
                "value" => $attValue,
1052
                "level" => Options::LEVEL_PROFILE,
1053
                "row_id" => 0,
1054
                "flag" => NULL,
1055
            ];
1056
        }
1057
        return $retArray;
1058
    }
1059
1060
    /**
1061
     * Retrieves profile attributes stored in the database
1062
     * 
1063
     * @return array The attributes in one array
1064
     */
1065
    protected function addDatabaseAttributes()
1066
    {
1067
        $databaseAttributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row_id
1068
                FROM $this->entityOptionTable
1069
                WHERE $this->entityIdColumn = ?
1070
                AND device_id IS NULL AND eap_method_id = 0
1071
                ORDER BY option_name", "Profile");
1072
        return $databaseAttributes;
1073
    }
1074
}
1075