Passed
Push — release_2_0 ( cf22ac...b0f4f3 )
by Stefan
07:48
created

Federation   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 497
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 71
eloc 247
c 3
b 1
f 0
dl 0
loc 497
rs 2.7199

15 Methods

Rating   Name   Duplication   Size   Complexity  
B downloadStats() 0 33 8
A downloadStatsCore() 0 23 3
A updateFreshness() 0 4 2
A __construct() 0 52 5
B newIdP() 0 47 8
A requestCertificate() 0 8 1
C listExternalEntities() 0 67 16
A findCandidates() 0 16 5
A determineIdPIdByRealm() 0 21 4
A listTlsCertificates() 0 18 2
A listFederationAdmins() 0 15 5
A usortInstitution() 0 2 1
B listIdentityProviders() 0 38 8
A updateCertificateStatus() 0 11 2
A triggerRevocation() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like Federation 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 Federation, 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 Federation class.
25
 * 
26
 * @author Stefan Winter <[email protected]>
27
 * @author Tomasz Wolniewicz <[email protected]>
28
 * 
29
 * @package Developer
30
 * 
31
 */
32
33
namespace core;
34
35
use \Exception;
36
37
/**
38
 * This class represents an consortium federation.
39
 * 
40
 * It is semantically a country(!). Do not confuse this with a TLD; a federation
41
 * may span more than one TLD, and a TLD may be distributed across multiple federations.
42
 *
43
 * Example: a federation "fr" => "France" may also contain other TLDs which
44
 *              belong to France in spite of their different TLD
45
 * Example 2: Domains ending in .edu are present in multiple different
46
 *              federations
47
 *
48
 * @author Stefan Winter <[email protected]>
49
 * @author Tomasz Wolniewicz <[email protected]>
50
 *
51
 * @license see LICENSE file in root directory
52
 *
53
 * @package Developer
54
 */
55
class Federation extends EntityWithDBProperties {
56
57
    /**
58
     * the handle to the FRONTEND database (only needed for some stats access)
59
     * 
60
     * @var DBConnection
61
     */
62
    private $frontendHandle;
63
64
    /**
65
     * the top-level domain of the Federation
66
     * 
67
     * @var string
68
     */
69
    public $tld;
70
71
    /**
72
     * retrieve the statistics from the database in an internal array representation
73
     * 
74
     * @return array
75
     */
76
    private function downloadStatsCore() {
77
        $grossAdmin = 0;
78
        $grossUser = 0;
79
        $grossSilverbullet = 0;
80
        $dataArray = [];
81
        // first, find out which profiles belong to this federation
82
        $cohesionQuery = "SELECT downloads.device_id as dev_id, sum(downloads.downloads_user) as dl_user, sum(downloads.downloads_silverbullet) as dl_sb, sum(downloads.downloads_admin) as dl_admin FROM profile, institution, downloads WHERE profile.inst_id = institution.inst_id AND institution.country = ? AND profile.profile_id = downloads.profile_id group by device_id";
83
        $profilesList = $this->databaseHandle->exec($cohesionQuery, "s", $this->tld);
84
        $deviceArray = \devices\Devices::listDevices();
85
        // SELECT -> resource, no boolean
86
        while ($queryResult = mysqli_fetch_object(/** @scrutinizer ignore-type */ $profilesList)) {
87
            if (isset($deviceArray[$queryResult->dev_id])) {
88
                $displayName = $deviceArray[$queryResult->dev_id]['display'];
89
            } else { // this device has stats, but doesn't exist in current config. We don't even know its display name, so display its raw representation
90
                $displayName = sprintf(_("(discontinued) %s"), $queryResult->dev_id);
91
            }
92
            $dataArray[$displayName] = ["ADMIN" => $queryResult->dl_admin, "SILVERBULLET" => $queryResult->dl_sb, "USER" => $queryResult->dl_user];
93
            $grossAdmin = $grossAdmin + $queryResult->dl_admin;
94
            $grossSilverbullet = $grossSilverbullet + $queryResult->dl_sb;
95
            $grossUser = $grossUser + $queryResult->dl_user;
96
        }
97
        $dataArray["TOTAL"] = ["ADMIN" => $grossAdmin, "SILVERBULLET" => $grossSilverbullet, "USER" => $grossUser];
98
        return $dataArray;
99
    }
100
101
    /**
102
     * when a Federation attribute changes, invalidate caches of all IdPs 
103
     * in that federation (e.g. change of fed logo changes the actual 
104
     * installers)
105
     * 
106
     * @return void
107
     */
108
    public function updateFreshness() {
109
        $idplist = $this->listIdentityProviders();
110
        foreach ($idplist as $idpDetail) {
111
            $idpDetail['instance']->updateFreshness();
112
        }
113
    }
114
115
    /**
116
     * gets the download statistics for the federation
117
     * @param string $format either as an html *table* or *XML* or *JSON*
118
     * @return string|array
119
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
120
    public function downloadStats($format) {
121
        $data = $this->downloadStatsCore();
122
        $retstring = "";
123
124
        switch ($format) {
125
            case "table":
126
                foreach ($data as $device => $numbers) {
127
                    if ($device == "TOTAL") {
128
                        continue;
129
                    }
130
                    $retstring .= "<tr><td>$device</td><td>" . $numbers['ADMIN'] . "</td><td>" . $numbers['SILVERBULLET'] . "</td><td>" . $numbers['USER'] . "</td></tr>";
131
                }
132
                $retstring .= "<tr><td><strong>TOTAL</strong></td><td><strong>" . $data['TOTAL']['ADMIN'] . "</strong></td><td><strong>" . $data['TOTAL']['SILVERBULLET'] . "</strong></td><td><strong>" . $data['TOTAL']['USER'] . "</strong></td></tr>";
133
                break;
134
            case "XML":
135
                // the calls to date() operate on current date, so there is no chance for a FALSE to be returned. Silencing scrutinizer.
136
                $retstring .= "<federation id='$this->tld' ts='" . /** @scrutinizer ignore-type */ date("Y-m-d") . "T" . /** @scrutinizer ignore-type */ date("H:i:s") . "'>\n";
137
                foreach ($data as $device => $numbers) {
138
                    if ($device == "TOTAL") {
139
                        continue;
140
                    }
141
                    $retstring .= "  <device name='" . $device . "'>\n    <downloads group='admin'>" . $numbers['ADMIN'] . "</downloads>\n    <downloads group='managed_idp'>" . $numbers['SILVERBULLET'] . "</downloads>\n    <downloads group='user'>" . $numbers['USER'] . "</downloads>\n  </device>";
142
                }
143
                $retstring .= "<total>\n  <downloads group='admin'>" . $data['TOTAL']['ADMIN'] . "</downloads>\n  <downloads group='managed_idp'>" . $data['TOTAL']['SILVERBULLET'] . "</downloads>\n  <downloads group='user'>" . $data['TOTAL']['USER'] . "</downloads>\n</total>\n";
144
                $retstring .= "</federation>";
145
                break;
146
            case "array":
147
                return $data;
148
            default:
149
                throw new Exception("Statistics can be requested only in 'table' or 'XML' format!");
150
        }
151
152
        return $retstring;
153
    }
154
155
    /**
156
     *
157
     * Constructs a Federation object.
158
     *
159
     * @param string $fedname textual representation of the Federation object
160
     *                        Example: "lu" (for Luxembourg)
161
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
162
    public function __construct($fedname) {
163
164
        // initialise the superclass variables
165
166
        $this->databaseType = "INST";
167
        $this->entityOptionTable = "federation_option";
168
        $this->entityIdColumn = "federation_id";
169
170
        $cat = new CAT();
171
        if (!isset($cat->knownFederations[$fedname])) {
172
            throw new Exception("This federation is not known to the system!");
173
        }
174
        $this->identifier = 0; // we do not use the numeric ID of a federation
175
        $this->tld = $fedname;
176
        $this->name = $cat->knownFederations[$this->tld];
177
178
        parent::__construct(); // we now have access to our database handle
179
180
        $handle = DBConnection::handle("FRONTEND");
181
        if ($handle instanceof DBConnection) {
182
            $this->frontendHandle = $handle;
183
        } else {
184
            throw new Exception("This database type is never an array!");
185
        }
186
        // fetch attributes from DB; populates $this->attributes array
187
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
188
                                            FROM $this->entityOptionTable
189
                                            WHERE $this->entityIdColumn = ?
190
                                            ORDER BY option_name", "FED");
191
192
193
        $this->attributes[] = array("name" => "internal:country",
194
            "lang" => NULL,
195
            "value" => $this->tld,
196
            "level" => "FED",
197
            "row" => 0,
198
            "flag" => NULL);
199
200
        if (CONFIG['FUNCTIONALITY_LOCATIONS']['CONFASSISTANT_RADIUS'] != 'LOCAL' && CONFIG['FUNCTIONALITY_LOCATIONS']['CONFASSISTANT_SILVERBULLET'] == 'LOCAL') {
201
            // this instance exclusively does SB, so it is not necessary to ask
202
            // fed ops whether they want to enable it or not. So always add it
203
            // to the list of fed attributes
204
            $this->attributes[] = array("name" => "fed:silverbullet",
205
                "lang" => NULL,
206
                "value" => "on",
207
                "level" => "FED",
208
                "row" => 0,
209
                "flag" => NULL);
210
        }
211
212
        $this->idpListActive = [];
213
        $this->idpListAll = [];
214
    }
215
216
    /**
217
     * Creates a new IdP inside the federation.
218
     * 
219
     * @param string $ownerId       Persistent identifier of the user for whom this IdP is created (first administrator)
220
     * @param string $level         Privilege level of the first administrator (was he blessed by a federation admin or a peer?)
221
     * @param string $mail          e-mail address with which the user was invited to administer (useful for later user identification if the user chooses a "funny" real name)
222
     * @param string $bestnameguess name of the IdP, if already known, in the best-match language
223
     * @return int identifier of the new IdP
224
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
225
    public function newIdP($ownerId, $level, $mail = NULL, $bestnameguess = NULL) {
226
        $this->databaseHandle->exec("INSERT INTO institution (country) VALUES('$this->tld')");
227
        $identifier = $this->databaseHandle->lastID();
228
229
        if ($identifier == 0 || !$this->loggerInstance->writeAudit($ownerId, "NEW", "IdP $identifier")) {
230
            $text = "<p>Could not create a new " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_inst'] . "!</p>";
231
            echo $text;
232
            throw new Exception($text);
233
        }
234
235
        if ($ownerId != "PENDING") {
236
            if ($mail === NULL) {
237
                throw new Exception("New IdPs in a federation need a mail address UNLESS created by API without OwnerId");
238
            }
239
            $this->databaseHandle->exec("INSERT INTO ownership (user_id,institution_id, blesslevel, orig_mail) VALUES(?,?,?,?)", "siss", $ownerId, $identifier, $level, $mail);
240
        }
241
        if ($bestnameguess === NULL) {
242
            $bestnameguess = "(no name yet, identifier $identifier)";
243
        }
244
        $admins = $this->listFederationAdmins();
245
246
        // notify the fed admins...
247
248
        foreach ($admins as $id) {
249
            $user = new User($id);
250
            /// arguments are: 1. nomenclature for "institution"
251
            //                 2. IdP name; 
252
            ///                3. consortium name (e.g. eduroam); 
253
            ///                4. federation shortname, e.g. "LU"; 
254
            ///                5. product name (e.g. eduroam CAT); 
255
            ///                6. product long name (e.g. eduroam Configuration Assistant Tool)
256
            $message = sprintf(_("Hi,
257
258
the invitation for the new %s %s in your %s federation %s has been used and the IdP was created in %s.
259
260
We thought you might want to know.
261
262
Best regards,
263
264
%s"), common\Entity::$nomenclature_inst, $bestnameguess, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], strtoupper($this->tld), CONFIG['APPEARANCE']['productname'], CONFIG['APPEARANCE']['productname_long']);
265
            $retval = $user->sendMailToUser(sprintf(_("%s in your federation was created"), common\Entity::$nomenclature_inst), $message);
266
            if ($retval === FALSE) {
267
                $this->loggerInstance->debug(2, "Mail to federation admin was NOT sent!\n");
268
            }
269
        }
270
271
        return $identifier;
272
    }
273
274
    private $idpListAll;
0 ignored issues
show
Coding Style Documentation introduced by
Missing member variable doc comment
Loading history...
275
    private $idpListActive;
0 ignored issues
show
Coding Style Documentation introduced by
Missing member variable doc comment
Loading history...
276
277
    /**
278
     * fetches all known certificate information for RADIUS/TLS certs from the DB
279
     * 
280
     * @return array
281
     */
282
    public function listTlsCertificates() {
283
        $certQuery = "SELECT ca_name, request_serial, distinguished_name, status, expiry, certificate, revocation_pin FROM federation_servercerts WHERE federation_id = ?";
284
        $upperTld = strtoupper($this->tld);
285
        $certList = $this->databaseHandle->exec($certQuery, "s", $upperTld);
286
        $retArray = [];
287
        // SELECT -> resource, not boolean
288
        while ($certListResult = mysqli_fetch_object(/** @scrutinizer ignore-type */ $certList)) {
289
            $retArray[] = [
290
                'CA' => $certListResult->ca_name,
291
                'REQSERIAL' => $certListResult->request_serial,
292
                'DN' => $certListResult->distinguished_name,
293
                'STATUS' => $certListResult->status,
294
                'EXPIRY' => $certListResult->expiry,
295
                'CERT' => $certListResult->certificate,
296
                'REVPIN' => $certListResult->revocation_pin,
297
                ];
298
        }
299
        return$retArray;
300
    }
301
    
302
    public function requestCertificate($csr, $expiryDays) {
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
303
        $revocationPin = common\Entity::randomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
304
        $newReq = new CertificationAuthorityEduPkiServer();
305
        $reqserial = $newReq->sendRequestToCa($csr, $revocationPin, $expiryDays);
306
        $reqQuery = "INSERT INTO federation_servercerts "
307
                . "(federation_id, ca_name, request_serial, distinguished_name, status, revocation_pin) "
308
                . "VALUES (?, 'eduPKI', ?, ?, 'REQUESTED', ?)";
309
        $this->databaseHandle->exec($reqQuery, "siss", $this->tld, $reqserial, $csr['SUBJECT'], $revocationPin);
310
    }
311
    
312
    public function updateCertificateStatus($reqSerial) {
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
313
        $ca = new CertificationAuthorityEduPkiServer();
314
        $entryInQuestion = $ca->pickupFinalCert($reqSerial, FALSE);
315
        if ($entryInQuestion === FALSE) {
316
            return; // no update to fetch
317
        }
318
        $certDetails = openssl_x509_parse($entryInQuestion['CERT']);
319
        echo $certDetails['full_details'];
320
        openssl_x509_export($entryInQuestion['CERT'], $pem);
321
        $updateQuery = "UPDATE federation_servercerts SET status = 'ISSUED', certificate = ?, expiry = ? WHERE ca_name = 'eduPKI' AND request_serial = ?";
322
        $this->databaseHandle->exec($updateQuery, "ssi", $pem, $certDetails['full_details']['NotAfter'], $reqSerial);
323
    }
324
    
325
    public function triggerRevocation($reqSerial) {
0 ignored issues
show
Coding Style introduced by
Missing function doc comment
Loading history...
326
        // revocation at the CA side works with the serial of the certificate, not the request
327
        // so find that out first
328
        $certInfoResource = $this->databaseHandle->exec("SELECT certificate FROM federation_servercerts WHERE ca_name = 'eduPKI' AND request_serial = ?", "i", $reqSerial);
329
        $certInfo = mysqli_fetch_row($certInfoResource);
0 ignored issues
show
Bug introduced by
It seems like $certInfoResource can also be of type true; however, parameter $result of mysqli_fetch_row() does only seem to accept mysqli_result, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

329
        $certInfo = mysqli_fetch_row(/** @scrutinizer ignore-type */ $certInfoResource);
Loading history...
330
        $certData = openssl_x509_parse($certInfo);
331
        $serial = $certData['full_details']['serialNumber'];
332
        $eduPki = new CertificationAuthorityEduPkiServer();
333
        $eduPki->revokeCertificate($serial);
334
        $this->databaseHandle->exec("UPDATE federation_servercerts SET status = 'REVOKED' WHERE ca_name = 'eduPKI' AND request_serial = ?", "i", $reqSerial);
335
    }
336
    
337
    /**
338
     * Lists all Identity Providers in this federation
339
     *
340
     * @param int $activeOnly if set to non-zero will list only those institutions which have some valid profiles defined.
341
     * @return array (Array of IdP instances)
342
     *
343
     */
344
    public function listIdentityProviders($activeOnly = 0) {
345
        // maybe we did this exercise before?
346
        if ($activeOnly != 0 && count($this->idpListActive) > 0) {
347
            return $this->idpListActive;
348
        }
349
        if ($activeOnly == 0 && count($this->idpListAll) > 0) {
350
            return $this->idpListAll;
351
        }
352
        // default query is:
353
        $allIDPs = $this->databaseHandle->exec("SELECT inst_id FROM institution
354
               WHERE country = '$this->tld' ORDER BY inst_id");
355
        // the one for activeOnly is much more complex:
356
        if ($activeOnly) {
357
            $allIDPs = $this->databaseHandle->exec("SELECT distinct institution.inst_id AS inst_id
358
               FROM institution
359
               JOIN profile ON institution.inst_id = profile.inst_id
360
               WHERE institution.country = '$this->tld' 
361
               AND profile.showtime = 1
362
               ORDER BY inst_id");
363
        }
364
365
        $returnarray = [];
366
        // SELECT -> resource, not boolean
367
        while ($idpQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allIDPs)) {
368
            $idp = new IdP($idpQuery->inst_id);
369
            $name = $idp->name;
370
            $idpInfo = ['entityID' => $idp->identifier,
371
                'title' => $name,
372
                'country' => strtoupper($idp->federation),
373
                'instance' => $idp];
374
            $returnarray[$idp->identifier] = $idpInfo;
375
        }
376
        if ($activeOnly != 0) { // we're only doing this once.
377
            $this->idpListActive = $returnarray;
378
        } else {
379
            $this->idpListAll = $returnarray;
380
        }
381
        return $returnarray;
382
    }
383
384
    /**
385
     * returns an array with information about the authorised administrators of the federation
386
     * 
387
     * @return array list of the admins of this federation
388
     */
389
    public function listFederationAdmins() {
390
        $returnarray = [];
391
        $query = "SELECT user_id FROM user_options WHERE option_name = 'user:fedadmin' AND option_value = ?";
392
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
393
            $query = "SELECT eptid as user_id FROM view_admin WHERE role = 'fedadmin' AND realm = ?";
394
        }
395
        $userHandle = DBConnection::handle("USER"); // we need something from the USER database for a change
396
        $upperFed = strtoupper($this->tld);
397
        // SELECT -> resource, not boolean
398
        $admins = $userHandle->exec($query, "s", $upperFed);
399
400
        while ($fedAdminQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
401
            $returnarray[] = $fedAdminQuery->user_id;
402
        }
403
        return $returnarray;
404
    }
405
406
    public const EDUROAM_DB_TYPE_IDP = "1";
407
    public const EDUROAM_DB_TYPE_SP = "2";
408
    public const EDUROAM_DB_TYPE_IDPSP = "3";
409
    
410
    /**
411
     * cross-checks in the EXTERNAL customer DB which institutions exist there for the federations
412
     * 
413
     * @param bool   $unmappedOnly if set to TRUE, only returns those which do not have a known mapping to our internally known institutions
414
     * @param string $type         type of institution to search for, see constants above
415
     * @return array
416
     */
417
    public function listExternalEntities($unmappedOnly, $type) {
418
        $returnarray = [];
419
420
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
421
            $usedarray = [];
422
            $externalHandle = DBConnection::handle("EXTERNAL");
423
            $query = "SELECT id_institution AS id, country, inst_realm as realmlist, name AS collapsed_name, contact AS collapsed_contact FROM view_active_institution WHERE country = ? AND ( type = '".Federation::EDUROAM_DB_TYPE_IDPSP."' OR type = ? )";
424
            $externals = $externalHandle->exec($query, "ss", $this->tld, $type);
425
            $syncstate = IdP::EXTERNAL_DB_SYNCSTATE_SYNCED;
426
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution 
427
                                                                                                     WHERE external_db_id IS NOT NULL 
428
                                                                                                     AND external_db_syncstate = ?", "i", $syncstate);
429
            $pendingInvite = $this->databaseHandle->exec("SELECT DISTINCT external_db_uniquehandle FROM invitations 
430
                                                                                                      WHERE external_db_uniquehandle IS NOT NULL 
431
                                                                                                      AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) 
432
                                                                                                      AND used = 0");
433
            // SELECT -> resource, no boolean
434
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
435
                $usedarray[] = $alreadyUsedQuery->external_db_id;
436
            }
437
            // SELECT -> resource, no boolean
438
            while ($pendingInviteQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $pendingInvite)) {
439
                if (!in_array($pendingInviteQuery->external_db_uniquehandle, $usedarray)) {
440
                    $usedarray[] = $pendingInviteQuery->external_db_uniquehandle;
441
                }
442
            }
443
            // was a SELECT query, so a resource and not a boolean
444
            while ($externalQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $externals)) {
445
                if (($unmappedOnly === TRUE) && (in_array($externalQuery->id, $usedarray))) {
446
                    continue;
447
                }
448
                $names = explode('#', $externalQuery->collapsed_name);
449
                // trim name list to current best language match
450
                $availableLanguages = [];
451
                foreach ($names as $name) {
452
                    $thislang = explode(': ', $name, 2);
453
                    $availableLanguages[$thislang[0]] = $thislang[1];
454
                }
455
                if (array_key_exists($this->languageInstance->getLang(), $availableLanguages)) {
456
                    $thelangauge = $availableLanguages[$this->languageInstance->getLang()];
457
                } else if (array_key_exists("en", $availableLanguages)) {
458
                    $thelangauge = $availableLanguages["en"];
459
                } else { // whatever. Pick one out of the list
460
                    $thelangauge = array_pop($availableLanguages);
461
                }
462
                $contacts = explode('#', $externalQuery->collapsed_contact);
463
464
465
                $mailnames = "";
466
                foreach ($contacts as $contact) {
467
                    $matches = [];
468
                    preg_match("/^n: (.*), e: (.*), p: .*$/", $contact, $matches);
469
                    if ($matches[2] != "") {
470
                        if ($mailnames != "") {
471
                            $mailnames .= ", ";
472
                        }
473
                        // extracting real names is nice, but the <> notation
474
                        // really gets screwed up on POSTs and HTML safety
475
                        // so better not do this; use only mail addresses
476
                        $mailnames .= $matches[2];
477
                    }
478
                }
479
                $returnarray[] = ["ID" => $externalQuery->id, "name" => $thelangauge, "contactlist" => $mailnames, "country" => $externalQuery->country, "realmlist" => $externalQuery->realmlist];
480
            }
481
            usort($returnarray, array($this, "usortInstitution"));
482
        }
483
        return $returnarray;
484
    }
485
486
    const UNKNOWN_IDP = -1;
487
    const AMBIGUOUS_IDP = -2;
488
489
    /**
490
     * for a MySQL list of institutions, find an institution or find out that
491
     * there is no single best match
492
     * 
493
     * @param \mysqli_result $dbResult the query object to work with
494
     * @param string         $country  used to return the country of the inst, if can be found out
495
     * @return int the identifier of the inst, or one of the special return values if unsuccessful
496
     */
497
    private static function findCandidates(\mysqli_result $dbResult, &$country) {
498
        $retArray = [];
499
        while ($row = mysqli_fetch_object($dbResult)) {
500
            if (!in_array($row->id, $retArray)) {
501
                $retArray[] = $row->id;
502
                $country = strtoupper($row->country);
503
            }
504
        }
505
        if (count($retArray) <= 0) {
506
            return Federation::UNKNOWN_IDP;
507
        }
508
        if (count($retArray) > 1) {
509
            return Federation::AMBIGUOUS_IDP;
510
        }
511
512
        return array_pop($retArray);
513
    }
514
515
    /**
516
     * If we are running diagnostics, our input from the user is the realm. We
517
     * need to find out which IdP this realm belongs to.
518
     * @param string $realm the realm to search for
519
     * @return array an array with two entries, CAT ID and DB ID, with either the respective ID of the IdP in the system, or UNKNOWN_IDP or AMBIGUOUS_IDP
520
     */
521
    public static function determineIdPIdByRealm($realm) {
522
        $country = NULL;
523
        $candidatesExternalDb = Federation::UNKNOWN_IDP;
524
        $dbHandle = DBConnection::handle("INST");
525
        $realmSearchStringCat = "%@$realm";
526
        $candidateCatQuery = $dbHandle->exec("SELECT p.profile_id as id, i.country as country FROM profile p, institution i WHERE p.inst_id = i.inst_id AND p.realm LIKE ?", "s", $realmSearchStringCat);
527
        // this is a SELECT returning a resource, not a boolean
528
        $candidatesCat = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateCatQuery, $country);
529
530
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED        
531
            $externalHandle = DBConnection::handle("EXTERNAL");
532
            $realmSearchStringDb1 = "$realm";
533
            $realmSearchStringDb2 = "%,$realm";
534
            $realmSearchStringDb3 = "$realm,%";
535
            $realmSearchStringDb4 = "%,$realm,%";
536
            $candidateExternalQuery = $externalHandle->exec("SELECT id_institution as id, country FROM view_active_idp_institution WHERE inst_realm LIKE ? or inst_realm LIKE ? or inst_realm LIKE ? or inst_realm LIKE ?", "ssss", $realmSearchStringDb1, $realmSearchStringDb2, $realmSearchStringDb3, $realmSearchStringDb4);
537
            // SELECT -> resource, not boolean
538
            $candidatesExternalDb = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateExternalQuery, $country);
539
        }
540
541
        return ["CAT" => $candidatesCat, "EXTERNAL" => $candidatesExternalDb, "FEDERATION" => $country];
542
    }
543
544
    /**
545
     * helper function to sort institutions by their name
546
     * @param array $a an array with institution a's information
547
     * @param array $b an array with institution b's information
548
     * @return int the comparison result
549
     */
550
    private function usortInstitution($a, $b) {
551
        return strcasecmp($a["name"], $b["name"]);
552
    }
553
554
}
555