Passed
Push — master ( 05ae42...4e09ff )
by Stefan
07:31
created

Federation   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 57
dl 0
loc 375
rs 5.1724
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A updateFreshness() 0 1 1
A downloadStatsCore() 0 18 2
C downloadStats() 0 33 8
C listExternalEntities() 0 69 16
B findCandidates() 0 16 5
A determineIdPIdByRealm() 0 21 4
A usortInstitution() 0 2 1
B listFederationAdmins() 0 15 5
B __construct() 0 36 2
C listIdentityProviders() 0 38 8
B newIdP() 0 17 5

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
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
/**
13
 * This file contains the Federation class.
14
 * 
15
 * @author Stefan Winter <[email protected]>
16
 * @author Tomasz Wolniewicz <[email protected]>
17
 * 
18
 * @package Developer
19
 * 
20
 */
21
22
namespace core;
23
24
use \Exception;
25
26
/**
27
 * This class represents an consortium federation.
28
 * 
29
 * It is semantically a country(!). Do not confuse this with a TLD; a federation
30
 * may span more than one TLD, and a TLD may be distributed across multiple federations.
31
 *
32
 * Example: a federation "fr" => "France" may also contain other TLDs which
33
 *              belong to France in spite of their different TLD
34
 * Example 2: Domains ending in .edu are present in multiple different
35
 *              federations
36
 *
37
 * @author Stefan Winter <[email protected]>
38
 * @author Tomasz Wolniewicz <[email protected]>
39
 *
40
 * @license see LICENSE file in root directory
41
 *
42
 * @package Developer
43
 */
44
class Federation extends EntityWithDBProperties {
45
46
    /**
47
     * the handle to the FRONTEND database (only needed for some stats access)
48
     * 
49
     * @var DBConnection
50
     */
51
    private $frontendHandle;
52
53
    /**
54
     * the top-level domain of the Federation
55
     * 
56
     * @var string
57
     */
58
    public $tld;
59
60
    /**
61
     * retrieve the statistics from the database in an internal array representation
62
     * 
63
     * @return array
64
     */
65
    private function downloadStatsCore() {
66
        $grossAdmin = 0;
67
        $grossUser = 0;
68
        $grossSilverbullet = 0;
69
        $dataArray = [];
70
        // first, find out which profiles belong to this federation
71
        $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";
72
        $profilesList = $this->databaseHandle->exec($cohesionQuery, "s", $this->tld);
73
        $deviceArray = \devices\Devices::listDevices();
74
        // SELECT -> resource, no boolean
75
        while ($queryResult = mysqli_fetch_object(/** @scrutinizer ignore-type */ $profilesList)) {
76
            $dataArray[$deviceArray[$queryResult->dev_id]['display']] = ["ADMIN" => $queryResult->dl_admin, "SILVERBULLET" => $queryResult->dl_sb, "USER" => $queryResult->dl_user];
77
            $grossAdmin = $grossAdmin + $queryResult->dl_admin;
78
            $grossSilverbullet = $grossSilverbullet + $queryResult->dl_sb;
79
            $grossUser = $grossUser + $queryResult->dl_user;
80
        }
81
        $dataArray["TOTAL"] = ["ADMIN" => $grossAdmin, "SILVERBULLET" => $grossSilverbullet, "USER" => $grossUser];
82
        return $dataArray;
83
    }
84
85
    /**
86
     * NOOP on Federations, but have to override the abstract parent method
87
     */
88
    public function updateFreshness() {
89
        // Federation is always fresh
90
    }
91
92
    /**
93
     * gets the download statistics for the federation
94
     * @param string $format either as an html *table* or *XML* or *JSON*
95
     * @return string|array
96
     */
97
    public function downloadStats($format) {
98
        $data = $this->downloadStatsCore();
99
        $retstring = "";
100
101
        switch ($format) {
102
            case "table":
103
                foreach ($data as $device => $numbers) {
104
                    if ($device == "TOTAL") {
105
                        continue;
106
                    }
107
                    $retstring .= "<tr><td>$device</td><td>" . $numbers['ADMIN'] . "</td><td>" . $numbers['SILVERBULLET'] . "</td><td>" . $numbers['USER'] . "</td></tr>";
108
                }
109
                $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>";
110
                break;
111
            case "XML":
112
                // the calls to date() operate on current date, so there is no chance for a FALSE to be returned. Silencing scrutinizer.
113
                $retstring .= "<federation id='$this->tld' ts='" . /** @scrutinizer ignore-type */ date("Y-m-d") . "T" . /** @scrutinizer ignore-type */ date("H:i:s") . "'>\n";
114
                foreach ($data as $device => $numbers) {
115
                    if ($device == "TOTAL") {
116
                        continue;
117
                    }
118
                    $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>";
119
                }
120
                $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";
121
                $retstring .= "</federation>";
122
                break;
123
            case "array":
124
                return $data;
125
            default:
126
                throw new Exception("Statistics can be requested only in 'table' or 'XML' format!");
127
        }
128
129
        return $retstring;
130
    }
131
132
    /**
133
     *
134
     * Constructs a Federation object.
135
     *
136
     * @param string $fedname - textual representation of the Federation object
137
     *        Example: "lu" (for Luxembourg)
138
     */
139
    public function __construct($fedname) {
140
141
        // initialise the superclass variables
142
143
        $this->databaseType = "INST";
144
        $this->entityOptionTable = "federation_option";
145
        $this->entityIdColumn = "federation_id";
146
147
        $cat = new CAT();
148
        if (!isset($cat->knownFederations[$fedname])) {
149
            throw new Exception("This federation is not known to the system!");
150
        }
151
        $this->identifier = 0; // we do not use the numeric ID of a federation
152
        $this->tld = $fedname;
153
        $this->name = $cat->knownFederations[$this->tld];
154
155
        parent::__construct(); // we now have access to our database handle
156
157
        $this->frontendHandle = DBConnection::handle("FRONTEND");
158
159
        // fetch attributes from DB; populates $this->attributes array
160
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
161
                                            FROM $this->entityOptionTable
162
                                            WHERE $this->entityIdColumn = ?
163
                                            ORDER BY option_name", "FED");
164
165
166
        $this->attributes[] = array("name" => "internal:country",
167
            "lang" => NULL,
168
            "value" => $this->tld,
169
            "level" => "FED",
170
            "row" => 0,
171
            "flag" => NULL);
172
        
173
        $this->idpListActive = [];
174
        $this->idpListAll = [];
175
    }
176
177
    /**
178
     * Creates a new IdP inside the federation.
179
     * 
180
     * @param string $ownerId Persistent identifier of the user for whom this IdP is created (first administrator)
181
     * @param string $level Privilege level of the first administrator (was he blessed by a federation admin or a peer?)
182
     * @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)
183
     * @return int identifier of the new IdP
184
     */
185
    public function newIdP($ownerId, $level, $mail = NULL) {
186
        $this->databaseHandle->exec("INSERT INTO institution (country) VALUES('$this->tld')");
187
        $identifier = $this->databaseHandle->lastID();
188
189
        if ($identifier == 0 || !$this->loggerInstance->writeAudit($ownerId, "NEW", "IdP $identifier")) {
190
            $text = "<p>Could not create a new " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_inst'] . "!</p>";
191
            echo $text;
192
            throw new Exception($text);
193
        }
194
195
        if ($ownerId != "PENDING") {
196
            if ($mail === NULL) {
197
                throw new Exception("New IdPs in a federation need a mail address UNLESS created by API without OwnerId");
198
            }
199
            $this->databaseHandle->exec("INSERT INTO ownership (user_id,institution_id, blesslevel, orig_mail) VALUES(?,?,?,?)", "siss", $ownerId, $identifier, $level, $mail);
200
        }
201
        return $identifier;
202
    }
203
204
    private $idpListAll;
205
    private $idpListActive;
206
    
207
    /**
208
     * Lists all Identity Providers in this federation
209
     *
210
     * @param int $activeOnly if set to non-zero will list only those institutions which have some valid profiles defined.
211
     * @return array (Array of IdP instances)
212
     *
213
     */
214
    public function listIdentityProviders($activeOnly = 0) {
215
        // maybe we did this exercise before?
216
        if ($activeOnly != 0 && count($this->idpListActive) > 0) {
217
            return $this->idpListActive;
218
        }
219
        if ($activeOnly == 0 && count($this->idpListAll) > 0) {
220
            return $this->idpListAll;
221
        }
222
        // default query is:
223
        $allIDPs = $this->databaseHandle->exec("SELECT inst_id FROM institution
224
               WHERE country = '$this->tld' ORDER BY inst_id");
225
        // the one for activeOnly is much more complex:
226
        if ($activeOnly) {
227
            $allIDPs = $this->databaseHandle->exec("SELECT distinct institution.inst_id AS inst_id
228
               FROM institution
229
               JOIN profile ON institution.inst_id = profile.inst_id
230
               WHERE institution.country = '$this->tld' 
231
               AND profile.showtime = 1
232
               ORDER BY inst_id");
233
        }
234
235
        $returnarray = [];
236
        // SELECT -> resource, not boolean
237
        while ($idpQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allIDPs)) {
238
            $idp = new IdP($idpQuery->inst_id);
239
            $name = $idp->name;
240
            $idpInfo = ['entityID' => $idp->identifier,
241
                'title' => $name,
242
                'country' => strtoupper($idp->federation),
243
                'instance' => $idp];
244
            $returnarray[$idp->identifier] = $idpInfo;
245
        }
246
        if ($activeOnly != 0) { // we're only doing this once.
247
            $this->idpListActive = $returnarray;
248
        } else {
249
            $this->idpListAll = $returnarray;
250
        }
251
        return $returnarray;
252
    }
253
254
    /**
255
     * returns an array with information about the authorised administrators of the federation
256
     * 
257
     * @return array
258
     */
259
    public function listFederationAdmins() {
260
        $returnarray = [];
261
        $query = "SELECT user_id FROM user_options WHERE option_name = 'user:fedadmin' AND option_value = ?";
262
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
263
            $query = "SELECT eptid as user_id FROM view_admin WHERE role = 'fedadmin' AND realm = ?";
264
        }
265
        $userHandle = DBConnection::handle("USER"); // we need something from the USER database for a change
266
        $upperFed = strtoupper($this->tld);
267
        // SELECT -> resource, not boolean
268
        $admins = $userHandle->exec($query, "s", $upperFed);
269
270
        while ($fedAdminQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
271
            $returnarray[] = $fedAdminQuery->user_id;
272
        }
273
        return $returnarray;
274
    }
275
276
    /**
277
     * cross-checks in the EXTERNAL customer DB which institutions exist there for the federations
278
     * 
279
     * @param bool $unmappedOnly if set to TRUE, only returns those which do not have a known mapping to our internally known institutions
280
     * @return array
281
     */
282
    public function listExternalEntities($unmappedOnly) {
283
        $returnarray = [];
284
285
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
286
            $usedarray = [];
287
            $query = "SELECT id_institution AS id, country, inst_realm as realmlist, name AS collapsed_name, contact AS collapsed_contact FROM view_active_idp_institution WHERE country = ?";
288
289
290
            $externalHandle = DBConnection::handle("EXTERNAL");
291
            $externals = $externalHandle->exec($query, "s", $this->tld);
292
            $syncstate = IdP::EXTERNAL_DB_SYNCSTATE_SYNCED;
293
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution 
294
                                                                                                     WHERE external_db_id IS NOT NULL 
295
                                                                                                     AND external_db_syncstate = ?", "i", $syncstate);
296
            $pendingInvite = $this->databaseHandle->exec("SELECT DISTINCT external_db_uniquehandle FROM invitations 
297
                                                                                                      WHERE external_db_uniquehandle IS NOT NULL 
298
                                                                                                      AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) 
299
                                                                                                      AND used = 0");
300
            // SELECT -> resource, no boolean
301
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
302
                $usedarray[] = $alreadyUsedQuery->external_db_id;
303
            }
304
            // SELECT -> resource, no boolean
305
            while ($pendingInviteQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $pendingInvite)) {
306
                if (!in_array($pendingInviteQuery->external_db_uniquehandle, $usedarray)) {
307
                    $usedarray[] = $pendingInviteQuery->external_db_uniquehandle;
308
                }
309
            }
310
            // was a SELECT query, so a resource and not a boolean
311
            while ($externalQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $externals)) {
312
                if (($unmappedOnly === TRUE) && (in_array($externalQuery->id, $usedarray))) {
313
                    continue;
314
                }
315
                $names = explode('#', $externalQuery->collapsed_name);
316
                // trim name list to current best language match
317
                $availableLanguages = [];
318
                foreach ($names as $name) {
319
                    $thislang = explode(': ', $name, 2);
320
                    $availableLanguages[$thislang[0]] = $thislang[1];
321
                }
322
                if (array_key_exists($this->languageInstance->getLang(), $availableLanguages)) {
323
                    $thelangauge = $availableLanguages[$this->languageInstance->getLang()];
324
                } else if (array_key_exists("en", $availableLanguages)) {
325
                    $thelangauge = $availableLanguages["en"];
326
                } else { // whatever. Pick one out of the list
327
                    $thelangauge = array_pop($availableLanguages);
328
                }
329
                $contacts = explode('#', $externalQuery->collapsed_contact);
330
331
332
                $mailnames = "";
333
                foreach ($contacts as $contact) {
334
                    $matches = [];
335
                    preg_match("/^n: (.*), e: (.*), p: .*$/", $contact, $matches);
336
                    if ($matches[2] != "") {
337
                        if ($mailnames != "") {
338
                            $mailnames .= ", ";
339
                        }
340
                        // extracting real names is nice, but the <> notation
341
                        // really gets screwed up on POSTs and HTML safety
342
                        // so better not do this; use only mail addresses
343
                        $mailnames .= $matches[2];
344
                    }
345
                }
346
                $returnarray[] = ["ID" => $externalQuery->id, "name" => $thelangauge, "contactlist" => $mailnames, "country" => $externalQuery->country, "realmlist" => $externalQuery->realmlist];
347
            }
348
            usort($returnarray, array($this, "usortInstitution"));
349
        }
350
        return $returnarray;
351
    }
352
353
    const UNKNOWN_IDP = -1;
354
    const AMBIGUOUS_IDP = -2;
355
356
    /**
357
     * for a MySQL list of institutions, find an institution or find out that
358
     * there is no single best match
359
     * 
360
     * @param \mysqli_result $dbResult
361
     * @param string $country used to return the country of the inst, if can be found out
362
     * @return int the identifier of the inst, or one of the special return values if unsuccessful
363
     */
364
    private static function findCandidates(\mysqli_result $dbResult, &$country) {
365
        $retArray = [];
366
        while ($row = mysqli_fetch_object($dbResult)) {
367
            if (!in_array($row->id, $retArray)) {
368
                $retArray[] = $row->id;
369
                $country = strtoupper($row->country);
370
            }
371
        }
372
        if (count($retArray) <= 0) {
373
            return Federation::UNKNOWN_IDP;
374
        }
375
        if (count($retArray) > 1) {
376
            return Federation::AMBIGUOUS_IDP;
377
        }
378
379
        return array_pop($retArray);
380
    }
381
382
    /**
383
     * If we are running diagnostics, our input from the user is the realm. We
384
     * need to find out which IdP this realm belongs to.
385
     * @param string $realm the realm to search for
386
     * @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
387
     */
388
    public static function determineIdPIdByRealm($realm) {
389
        $country = NULL;
390
        $candidatesExternalDb = Federation::UNKNOWN_IDP;
391
        $dbHandle = DBConnection::handle("INST");
392
        $realmSearchStringCat = "%@$realm";
393
        $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);
394
        // this is a SELECT returning a resource, not a boolean
395
        $candidatesCat = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateCatQuery, $country);
396
397
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED        
398
            $externalHandle = DBConnection::handle("EXTERNAL");
399
            $realmSearchStringDb1 = "$realm";
400
            $realmSearchStringDb2 = "%,$realm";
401
            $realmSearchStringDb3 = "$realm,%";
402
            $realmSearchStringDb4 = "%,$realm,%";
403
            $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);
404
            // SELECT -> resource, not boolean
405
            $candidatesExternalDb = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateExternalQuery, $country);
406
        }
407
408
        return ["CAT" => $candidatesCat, "EXTERNAL" => $candidatesExternalDb, "FEDERATION" => $country];
409
    }
410
411
    /**
412
     * helper function to sort institutions by their name
413
     * @param array $a an array with institution a's information
414
     * @param array $b an array with institution b's information
415
     * @return int the comparison result
416
     */
417
    private function usortInstitution($a, $b) {
418
        return strcasecmp($a["name"], $b["name"]);
419
    }
420
421
}
422