Passed
Push — master ( 14cefc...1cb0a0 )
by Stefan
05:05 queued 25s
created

Federation   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 54
dl 0
loc 372
rs 6.8539
c 0
b 0
f 0

11 Methods

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