Test Failed
Push — master ( 1b57e8...312ba7 )
by Tomasz
10:03
created

IdP   F

Complexity

Total Complexity 86

Size/Duplication

Total Lines 548
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 86
eloc 224
c 1
b 1
f 0
dl 0
loc 548
rs 2

21 Methods

Rating   Name   Duplication   Size   Complexity  
A listProfiles() 0 14 3
A __construct() 0 43 5
A listOwners() 0 9 2
A listDeployments() 0 16 4
A destroy() 0 36 5
A updateFreshness() 0 5 1
A maxDeploymentStatus() 0 8 2
A maxProfileStatus() 0 20 5
A maxOpenRoamingStatus() 0 8 3
A eligibility() 0 9 3
A isPrimaryOwner() 0 8 4
B getExternalDBSyncCandidates() 0 34 11
A profileCount() 0 10 2
A deploymentCount() 0 10 2
B significantChanges() 0 27 8
A newDeployment() 0 12 3
A setExternalDBId() 0 8 5
A getExternalDBSyncState() 0 6 4
A removeExternalDBId() 0 6 5
A getExternalDBId() 0 13 5
A newProfile() 0 18 4

How to fix   Complexity   

Complex Class

Complex classes like IdP 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 IdP, 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 Federation, IdP and Profile classes.
25
 * These should be split into separate files later.
26
 *
27
 * @package Developer
28
 */
29
/**
30
 * 
31
 */
32
33
namespace core;
34
35
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...
36
37
/**
38
 * This class represents an Identity Provider (IdP).
39
 * IdPs have properties of their own, and may have one or more Profiles. The
40
 * profiles can override the institution-wide properties.
41
 *
42
 * @author Stefan Winter <[email protected]>
43
 * @author Tomasz Wolniewicz <[email protected]>
44
 *
45
 * @license see LICENSE file in root directory
46
 *
47
 * @package Developer
48
 */
49
class IdP extends EntityWithDBProperties
50
{
51
52
    const EXTERNAL_DB_SYNCSTATE_NOT_SYNCED = 0;
53
    const EXTERNAL_DB_SYNCSTATE_SYNCED = 1;
54
    const EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING = 2;
55
    const TYPE_IDP = 'IdP';
56
    const TYPE_SP = 'SP';
57
    const TYPE_IDPSP = 'IdPSP';
58
59
    /**
60
     *
61
     * @var integer synchronisation state with external database, if any
62
     */
63
    private $externalDbSyncstate;
64
65
    /**
66
     * The shortname of this IdP's federation
67
     * @var string 
68
     */
69
    public $federation;
70
71
    /**
72
     * The type of participant in DB enum notation
73
     * @var string
74
     */
75
    public $type;
76
77
    /**
78
     * Constructs an IdP object based on its details in the database.
79
     * Cannot be used to define a new IdP in the database! This happens via Federation::newIdP()
80
     *
81
     * @param int $instId the database row_id identifier
82
     * @throws Exception
83
     */
84
    public function __construct(int $instId)
85
    {
86
        $this->databaseType = "INST";
87
        parent::__construct(); // now databaseHandle and logging is available
88
        $this->entityOptionTable = "institution_option";
89
        $this->entityIdColumn = "institution_id";
90
91
        $this->identifier = $instId;
92
93
        $idp = $this->databaseHandle->exec("SELECT inst_id, country,external_db_syncstate, type FROM institution WHERE inst_id = $this->identifier");
94
        // SELECT -> returns resource, not boolean
95
        if (!$instQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idp)) {
96
            throw new Exception("IdP $this->identifier not found in database!");
97
        }
98
99
        $this->federation = $instQuery->country;
100
        $this->externalDbSyncstate = $instQuery->external_db_syncstate;
101
102
        // fetch attributes from DB; populates $this->attributes array
103
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row_id 
104
                                            FROM $this->entityOptionTable
105
                                            WHERE $this->entityIdColumn = ?  
106
                                            ORDER BY option_name", "IdP");
107
108
        $this->attributes[] = ["name" => "internal:country",
109
            "lang" => NULL,
110
            "value" => $this->federation,
111
            "level" => Options::LEVEL_IDP,
112
            "row_id" => 0,
113
            "flag" => NULL];
114
115
        $this->name = $this->languageInstance->getLocalisedValue($this->getAttributes('general:instname'));
116
        $eligibility = $this->eligibility($instQuery->type);
117
        if (in_array(IdP::ELIGIBILITY_IDP, $eligibility) && in_array(IdP::ELIGIBILITY_SP, $eligibility)) {
118
            $eligType = IdP::TYPE_IDPSP . "";
119
            $this->type = $eligType;
120
        } elseif (in_array(IdP::ELIGIBILITY_IDP, $eligibility)) {
121
            $eligType = IdP::TYPE_IDP . "";
122
        } else {
123
            $eligType = IdP::TYPE_SP . "";
124
        }
125
        $this->type = $eligType;
126
        $this->loggerInstance->debug(4, "--- END Constructing new IdP object $instId ... ---\n");
127
    }
128
129
    /**
130
     * This function retrieves all registered profiles for this IdP from the database
131
     *
132
     * @param bool $activeOnly if and set to non-zero will cause listing of only those institutions which have some valid profiles defined.
133
     * @return \core\AbstractProfile[] list of Profiles of this IdP
134
     */
135
    public function listProfiles(bool $activeOnly = FALSE)
136
    {
137
        $query = "SELECT profile_id FROM profile WHERE inst_id = $this->identifier" . ($activeOnly ? " AND showtime = 1" : "")." ORDER BY preference";
138
        $allProfiles = $this->databaseHandle->exec($query);
139
        $returnarray = [];
140
        // SELECT -> resource, not boolean
141
        while ($profileQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
142
            $oneProfile = ProfileFactory::instantiate($profileQuery->profile_id, $this);
143
            $oneProfile->institution = $this->identifier;
144
            $returnarray[] = $oneProfile;
145
        }
146
147
        $this->loggerInstance->debug(4, "listProfiles: " . /** @scrutinizer ignore-type */ print_r($returnarray, true));
148
        return $returnarray;
149
    }
150
151
    /**
152
     * This function retrieves all SP deployments for this organisation from the database
153
     *
154
     * @param bool $activeOnly if and set to non-zero will cause listing of only those institutions which have some valid profiles defined.
155
     * @return \core\AbstractDeployment[] list of deployments of this IdP
156
     */
157
    public function listDeployments(bool $activeOnly = FALSE, $deploymentId = null)
158
    {
159
        $deploymentSelect = '';
160
        if ($deploymentId !== null) {
161
            $deploymentSelect = " AND deployment_id = '$deploymentId'";
162
        }
163
        $query = "SELECT deployment_id FROM deployment WHERE inst_id = $this->identifier".$deploymentSelect.($activeOnly ? " AND status = ".AbstractDeployment::ACTIVE : "");
164
        $allDeployments = $this->databaseHandle->exec($query);
165
        $returnarray = [];
166
        // SELECT -> resource, not boolean
167
        while ($deploymentQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allDeployments)) {
168
            $returnarray[] = new DeploymentManaged($this, $deploymentQuery->deployment_id);
169
        }
170
171
        $this->loggerInstance->debug(4, "listDeployments: " . /** @scrutinizer ignore-type */ print_r($returnarray, true));
172
        return $returnarray;
173
    }
174
175
    const PROFILES_INCOMPLETE = 0;
176
    const PROFILES_CONFIGURED = 1;
177
    const PROFILES_SHOWTIME = 2;
178
    const PROFILES_REDIRECTED = 3;
179
    const DEPLOYMENTS_NONE = -1;
180
    const DEPLOYMENTS_INACTIVE = 0;
181
    const DEPLOYMENTS_ACTIVE = 1;
182
    
183
    const PROFILES_INDEX = [
184
        self::PROFILES_INCOMPLETE => 'PROFILES_INCOMPLETE',
185
        self::PROFILES_CONFIGURED => 'PROFILES_CONFIGURED',
186
        self::PROFILES_SHOWTIME => 'PROFILES_SHOWTIME',
187
        self::PROFILES_REDIRECTED => 'PROFILES_REDIRECTED',
188
    ];
189
190
    /**
191
     * looks through all the profiles of the inst and determines the highest prod-ready level among the profiles
192
     * @return int highest level of completeness of all the profiles of the inst or PROFILES_REDIRECTED if all profiles are redirected
193
     */
194
    
195
    public function maxProfileStatus()
196
    {
197
        $redirectProfileIds = [];
198
        $allProfileLevels = $this->databaseHandle->exec("SELECT profile_id, sufficient_config + showtime AS maxlevel FROM profile WHERE inst_id = $this->identifier ORDER BY maxlevel DESC");
199
        // SELECT yields a resource, not a boolean
200
        if ($allProfileLevels->num_rows == 0 ) {
201
            return self::PROFILES_INCOMPLETE;
202
        }
203
        $allProfilesArray = $allProfileLevels->fetch_all(MYSQLI_ASSOC);
204
        $max_level = $allProfilesArray[0]['maxlevel'];        
205
        $redirectProfiles = $this->databaseHandle->exec("SELECT profile.profile_id as profile_id FROM profile JOIN profile_option ON profile.profile_id=profile_option.profile_id WHERE inst_id = $this->identifier AND profile.showtime=1 AND option_name='device-specific:redirect' AND device_id IS NULL");
206
        while ($res = $redirectProfiles->fetch_object()) {
207
            $redirectProfileIds[] = $res->profile_id;
208
        }        
209
        foreach ($allProfilesArray as $profile) {
210
            if (!in_array($profile['profile_id'], $redirectProfileIds)) {
211
                return($max_level);
212
            }            
213
        }
214
        return self::PROFILES_REDIRECTED;
215
    }
216
    
217
    /**
218
     * looks through deployments of the inst and determines the highest deployment status
219
     * @return int the highest level of completeness (-1 none defined, 0 non active, 1 active) 
220
     */
221
    public function maxDeploymentStatus()
222
    {
223
        $allDeployments = $this->databaseHandle->exec("SELECT MAX(status) as status FROM deployment WHERE inst_id = $this->identifier");
224
        $res = $allDeployments->fetch_object();
225
        if ($res->status === null) {
226
            return self::DEPLOYMENTS_NONE;
227
        }
228
        return $res->status;
229
    }
230
231
    /**
232
     * looks through all the profiles of the inst and determines the highest 
233
     * participation/conformance level for OpenRoaming
234
     * 
235
     * @return int highest level of completeness of all the profiles of the inst
236
     */
237
    public function maxOpenRoamingStatus()
238
    {
239
        $allProfiles = $this->databaseHandle->exec("SELECT MIN(openroaming) AS maxlevel FROM profile WHERE inst_id = $this->identifier");
240
        // SELECT yields a resource, not a boolean
241
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allProfiles)) {
242
            return (is_numeric($res->maxlevel) ? (int)$res->maxlevel : AbstractProfile::OVERALL_OPENROAMING_LEVEL_NO ); // insts without profiles should get a "NO"
243
        }
244
        return AbstractProfile::OVERALL_OPENROAMING_LEVEL_NO;
245
    }
246
    
247
    
248
    /** This function retrieves an array of authorised users which can
249
     * manipulate this institution.
250
     * 
251
     * @return array owners of the institution; numbered array with members ID, MAIL and LEVEL
252
     */
253
    public function listOwners()
254
    {
255
        $returnarray = [];
256
        $admins = $this->databaseHandle->exec("SELECT user_id, orig_mail, blesslevel FROM ownership WHERE institution_id = $this->identifier ORDER BY user_id");
257
        // SELECT -> resource, not boolean
258
        while ($ownerQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
259
            $returnarray[] = ['ID' => $ownerQuery->user_id, 'MAIL' => $ownerQuery->orig_mail, 'LEVEL' => $ownerQuery->blesslevel];
260
        }
261
        return $returnarray;
262
    }
263
264
    /**
265
     * Primary owners are allowed to invite other (secondary) admins to the institution
266
     * 
267
     * @param string $user ID of a logged-in user
268
     * @return boolean TRUE if this user is an admin with FED-level blessing
269
     */
270
    public function isPrimaryOwner($user)
271
    {
272
        foreach ($this->listOwners() as $oneOwner) {
273
            if ($oneOwner['ID'] == $user && $oneOwner['LEVEL'] == "FED") {
274
                return TRUE;
275
            }
276
        }
277
        return FALSE;
278
    }
279
280
    /**
281
     * This function gets the profile count for a given IdP.
282
     * 
283
     * The count could be retrieved from the listProfiles method
284
     * but this is less expensive.
285
     *
286
     * @return int profile count
287
     */
288
    public function profileCount()
289
    {
290
        $result = $this->databaseHandle->exec("SELECT profile_id FROM profile 
291
             WHERE inst_id = $this->identifier");
292
        // SELECT -> resource, not boolean
293
        $numberOfRows = mysqli_num_rows(/** @scrutinizer ignore-type */ $result);
294
        if (is_string($numberOfRows)) {
295
            throw new Exception("Number of profiles > PHP_MAX_INT?");
296
        }
297
        return $numberOfRows;
298
    }
299
300
    /**
301
     * This function gets the deployment count for a given IdP.
302
     *
303
     * @return int deployment count
304
     */
305
    public function deploymentCount()
306
    {
307
        $result = $this->databaseHandle->exec("SELECT deployment_id FROM deployment
308
             WHERE inst_id = $this->identifier");
309
        // SELECT -> resource, not boolean
310
        $numberOfRows = mysqli_num_rows(/** @scrutinizer ignore-type */ $result);
311
        if (is_string($numberOfRows)) {
312
            throw new Exception("Number of deployments > PHP_MAX_INT?");
313
        }
314
        return $numberOfRows;
315
    }
316
317
    const ELIGIBILITY_IDP = "IdP";
318
    const ELIGIBILITY_SP = "SP";
319
320
    /**
321
     * checks whether the participant is an IdP, an SP, or both.
322
     * 
323
     * @return array list of eligibilities
324
     */
325
    public function eligibility($type)
326
    {
327
        switch ($type) {
328
            case "IdP":
329
                return [IdP::ELIGIBILITY_IDP];
330
            case "SP":
331
                return [IdP::ELIGIBILITY_SP];
332
            default:
333
                return [IdP::ELIGIBILITY_IDP, IdP::ELIGIBILITY_SP];
334
        }
335
    }
336
337
    /**
338
     * This function sets the timestamp of last modification of the child profiles to the current timestamp.
339
     * 
340
     * This is needed for installer caching: all installers which are on disk 
341
     * must be re-created if an attribute changes. This timestamp here
342
     * is used to determine if the installer on disk is still new enough.
343
     * 
344
     * @return void
345
     */
346
    public function updateFreshness()
347
    {
348
        // freshness is always defined for *Profiles*
349
        // IdP needs to update timestamp of all its profiles if an IdP-wide attribute changed
350
        $this->databaseHandle->exec("UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE inst_id = '$this->identifier'");
351
    }
352
353
    /**
354
     * Adds a new profile to this IdP.
355
     * 
356
     * Only creates the DB entry for the Profile. If you want to add attributes later, see Profile::addAttribute().
357
     *
358
     * @param string $type exactly "RADIUS" or "SILVERBULLET", all other values throw an Exception
359
     * @return AbstractProfile|NULL new Profile object if successful, or NULL if an error occurred
360
     * @throws Exception
361
     */
362
    public function newProfile(string $type)
363
    {
364
        $this->databaseHandle->exec("INSERT INTO profile (inst_id) VALUES($this->identifier)");
365
        $identifier = $this->databaseHandle->lastID();
366
        if ($identifier > 0) {
367
            switch ($type) {
368
                case AbstractProfile::PROFILETYPE_RADIUS:
369
                    return new ProfileRADIUS($identifier, $this);
370
                case AbstractProfile::PROFILETYPE_SILVERBULLET:
371
                    $theProfile = new ProfileSilverbullet($identifier, $this);
372
                    $theProfile->addSupportedEapMethod(new \core\common\EAP(\core\common\EAP::EAPTYPE_SILVERBULLET), 1);
373
                    $theProfile->setRealm($this->identifier . "-" . $theProfile->identifier . "." . strtolower($this->federation) . strtolower(\config\ConfAssistant::SILVERBULLET['realm_suffix']));
374
                    return $theProfile;
375
                default:
376
                    throw new Exception("This type of profile is unknown and can not be added.");
377
            }
378
        }
379
        return NULL;
380
    }
381
382
    /**
383
     * Adds a new hotspot deployment to this IdP.
384
     * 
385
     * Only creates the DB entry for the deployment. If you want to add attributes later, see Profile::addAttribute().
386
     *
387
     * @param string $type       exactly "RADIUS-SP" or "MANAGED-SP", all other values throw an Exception
388
     * @param string $consortium name of the consortium to attach this *Managed* SP to
389
     * @return DeploymentManaged the newly created deployment
390
     * @throws Exception
391
     */
392
    public function newDeployment(string $type, string $consortium = "eduroam")
393
    {
394
        switch ($type) {
395
            case AbstractDeployment::DEPLOYMENTTYPE_CLASSIC:
396
                // classic deployment exist in the eduroam DB. We don't do anything here.
397
                throw new Exception("This type of deployment is handled externally and requesting it here makes no sense.");
398
            case AbstractDeployment::DEPLOYMENTTYPE_MANAGED:
399
                $this->databaseHandle->exec("INSERT INTO deployment (inst_id) VALUES($this->identifier)");
400
                $identifier = $this->databaseHandle->lastID();
401
                return new DeploymentManaged($this, $identifier, $consortium);
402
            default:
403
                throw new Exception("This type of deployment is unknown and can not be added.");
404
        }
405
    }
406
407
    /**
408
     * deletes the IdP and all its profiles
409
     * 
410
     * @return void
411
     * @throws Exception
412
     */
413
    public function destroy()
414
    {
415
        common\Entity::intoThePotatoes();
416
        /* delete all profiles */
417
        foreach ($this->listProfiles() as $profile) {
418
            $profile->destroy();
419
        }
420
        /* double-check that all profiles are gone */
421
        $profiles = $this->listProfiles();
422
423
        if (count($profiles) > 0) {
424
            throw new Exception("This IdP shouldn't have any profiles any more!");
425
        }
426
427
        $this->databaseHandle->exec("DELETE FROM ownership WHERE institution_id = $this->identifier");
428
        $this->databaseHandle->exec("DELETE FROM institution_option WHERE institution_id = $this->identifier");
429
        $this->databaseHandle->exec("DELETE FROM institution WHERE inst_id = $this->identifier");
430
431
        // notify federation admins
432
        if (\config\Master::MAILSETTINGS['notify_nro']) {
433
            $fed = new Federation($this->federation);
434
            foreach ($fed->listFederationAdmins() as $id) {
435
                $user = new User($id);
436
                $message = sprintf(_("Hi,
437
438
    the %s %s in your %s federation %s has been deleted from %s.
439
440
    We thought you might want to know.
441
442
    Best regards,
443
444
    %s"), common\Entity::$nomenclature_participant, $this->name, \config\ConfAssistant::CONSORTIUM['display_name'], strtoupper($fed->name), \config\Master::APPEARANCE['productname'], \config\Master::APPEARANCE['productname_long']);
445
                $user->sendMailToUser(sprintf(_("%s in your federation was deleted"), common\Entity::$nomenclature_participant), $message);
446
            }
447
        }
448
        common\Entity::outOfThePotatoes();
449
    }
450
451
    /**
452
     * Performs a lookup in an external database to determine matching entities to this IdP. 
453
     * 
454
     * The business logic of this function is roaming consortium specific; if no match algorithm is known for the consortium, FALSE is returned.
455
     * 
456
     * @param string $type which type of entity are you looking for?
457
     * @return array list of entities in external database that correspond to this IdP
458
     */
459
    public function getExternalDBSyncCandidates($type)
460
    {
461
        $usedarray = [];
462
        $matchingCandidates = [];
463
        $syncstate = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
464
        $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id, country FROM institution WHERE external_db_id IS NOT NULL AND external_db_syncstate = ?", "i", $syncstate);
465
        // SELECT -> resource, not boolean
466
        while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
467
            $usedarray[] = $alreadyUsedQuery->external_db_id;
468
        }
469
        if (\config\ConfAssistant::CONSORTIUM['name'] == "eduroam" && isset(\config\ConfAssistant::CONSORTIUM['deployment-voodoo']) && \config\ConfAssistant::CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
470
            // extract all institutions from the country
471
            $list = [];
472
            $lowerFed = strtolower($this->federation);
473
            $eduroamDb = new ExternalEduroamDBData();
474
            $candidateList = $eduroamDb->listExternalEntities($lowerFed, $type);
475
            // and split them into ID, LANG, NAME pairs (operating on a resource, not boolean)
476
            foreach ($candidateList as $oneCandidate) {
477
                if (in_array($oneCandidate['ID'], $usedarray)) {
478
                    continue;
479
                }
480
                $list[] = $oneCandidate;
481
            }
482
            // now see if any of the languages in CAT match the best one we have got from DB
483
            $mynames = $this->getAttributes("general:instname");
484
            foreach ($mynames as $onename) {
485
                foreach ($list as $listentry) {
486
                    if ($onename['value'] == $listentry['name'] && array_search($listentry['ID'], $matchingCandidates) === FALSE) {
487
                        $matchingCandidates[] = $listentry['ID'];
488
                    }
489
                }
490
            }
491
        }
492
        return $matchingCandidates;
493
    }
494
495
    /**
496
     * returns the state of sync with the external DB.
497
     * 
498
     * @return int
499
     */
500
    public function getExternalDBSyncState()
501
    {
502
        if (\config\ConfAssistant::CONSORTIUM['name'] == "eduroam" && isset(\config\ConfAssistant::CONSORTIUM['deployment-voodoo']) && \config\ConfAssistant::CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
503
            return $this->externalDbSyncstate;
504
        }
505
        return self::EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING;
506
    }
507
508
    /**
509
     * Retrieves the external DB identifier of this institution. Returns FALSE if no ID is known.
510
     * 
511
     * @return object|boolean the external identifier; or FALSE if no external ID is known
512
     */
513
    public function getExternalDBId()
514
    {
515
        if (\config\ConfAssistant::CONSORTIUM['name'] == "eduroam" && isset(\config\ConfAssistant::CONSORTIUM['deployment-voodoo']) && \config\ConfAssistant::CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
516
            $idQuery = $this->databaseHandle->exec("SELECT external_db_id, country FROM institution WHERE inst_id = $this->identifier AND external_db_syncstate = " . self::EXTERNAL_DB_SYNCSTATE_SYNCED);
517
            // SELECT -> it's a resource, not a boolean
518
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $idQuery) == 0) {
519
                return FALSE;
520
            }
521
            $externalIdQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $idQuery);
522
            $externalIdQuery->ROid = strtoupper($externalIdQuery->country).'01';
523
            return $externalIdQuery;
524
        }
525
        return FALSE;
526
    }
527
528
    /**
529
     * Associates the external DB id with a CAT id
530
     * 
531
     * @param string $identifier the external DB id, which can be alphanumeric
532
     * @param string $country federation identifier in the eduroam DB
533
     * @return void
534
     */
535
    public function setExternalDBId(string $identifier, $country)
536
    {
537
        if (\config\ConfAssistant::CONSORTIUM['name'] == "eduroam" && isset(\config\ConfAssistant::CONSORTIUM['deployment-voodoo']) && \config\ConfAssistant::CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
538
            $syncState = self::EXTERNAL_DB_SYNCSTATE_SYNCED;
539
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution WHERE country = ? AND external_db_id = ? AND external_db_syncstate = ?", "ssi", $country, $identifier, $syncState);
540
            // SELECT -> resource, not boolean
541
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $alreadyUsed) == 0) {
542
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = ?, external_db_syncstate = ? WHERE inst_id = ?", "sii", $identifier, $syncState, $this->identifier);
543
            }
544
        }
545
    }
546
547
    /**
548
     * removes the link between a CAT institution and the external DB
549
     * 
550
     * @return void
551
     */
552
    public function removeExternalDBId()
553
    {
554
        if (\config\ConfAssistant::CONSORTIUM['name'] == "eduroam" && isset(\config\ConfAssistant::CONSORTIUM['deployment-voodoo']) && \config\ConfAssistant::CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
555
            if ($this->getExternalDBId() !== FALSE) {
556
                $syncState = self::EXTERNAL_DB_SYNCSTATE_NOT_SYNCED;
557
                $this->databaseHandle->exec("UPDATE institution SET external_db_id = NULL, external_db_syncstate = ? WHERE inst_id = ?", "ii", $syncState, $this->identifier);
558
            }
559
        }
560
    }
561
562
    public const INSTNAME_CHANGED = 1;
563
564
    /**
565
     * 
566
     * @param IdP $old the IdP instance with the old state
567
     * @param IdP $new the IdP instance with the new state
568
     * @return array list of changed things, and details about the change
569
     */
570
    public static function significantChanges($old, $new)
571
    {
572
        // check if the name of the inst was changed (in any language)
573
        $retval = [];
574
        $baseline = [];
575
        $newvalues = [];
576
        foreach ($old->getAttributes("general:instname") as $oldname) {
577
            $baseline[$oldname['lang']] = $oldname['value'];
578
        }
579
        foreach ($new->getAttributes("general:instname") as $newname) {
580
            $newvalues[$newname['lang']] = $newname['value'];
581
        }
582
        foreach ($baseline as $lang => $value) {
583
            if (!key_exists($lang, $newvalues)) {
584
                $retval[IdP::INSTNAME_CHANGED] .= "#[Language " . strtoupper($lang) . "] DELETED";
585
            } else {
586
                if ($value != $newvalues[$lang]) {
587
                    $retval[IdP::INSTNAME_CHANGED] .= "#[Language " . strtoupper($lang) . "] CHANGED from '" . $baseline[$lang] . "' to '" . $newvalues[$lang] . "'";
588
                }
589
            }
590
        }
591
        foreach ($newvalues as $lang => $value) {
592
            if (!key_exists($lang, $baseline)) {
593
                $retval[IdP::INSTNAME_CHANGED] .= "#[Language " . strtoupper($lang) . "] ADDED as '" . $value . "'";
594
            }
595
        }
596
        return $retval;
597
    }
598
}
599