Passed
Push — master ( 186947...462fde )
by Stefan
07:04
created

Federation   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 396
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 181
dl 0
loc 396
rs 4.5599
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A updateFreshness() 0 4 2
A downloadStatsCore() 0 23 3
A __construct() 0 48 4
B downloadStats() 0 33 8
A listFederationAdmins() 0 15 5
B listIdentityProviders() 0 38 8
B newIdP() 0 47 8
B listExternalEntities() 0 34 10
A findCandidates() 0 16 5
A determineIdPIdByRealm() 0 21 4
A usortInstitution() 0 2 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
     */
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
     */
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
        $this->frontendHandle = DBConnection::handle("FRONTEND");
181
182
        // fetch attributes from DB; populates $this->attributes array
183
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
184
                                            FROM $this->entityOptionTable
185
                                            WHERE $this->entityIdColumn = ?
186
                                            ORDER BY option_name", "FED");
187
188
189
        $this->attributes[] = array("name" => "internal:country",
190
            "lang" => NULL,
191
            "value" => $this->tld,
192
            "level" => "FED",
193
            "row" => 0,
194
            "flag" => NULL);
195
196
        if (CONFIG['FUNCTIONALITY_LOCATIONS']['CONFASSISTANT_RADIUS'] != 'LOCAL' && CONFIG['FUNCTIONALITY_LOCATIONS']['CONFASSISTANT_SILVERBULLET'] == 'LOCAL') {
197
            // this instance exclusively does SB, so it is not necessary to ask
198
            // fed ops whether they want to enable it or not. So always add it
199
            // to the list of fed attributes
200
            $this->attributes[] = array("name" => "fed:silverbullet",
201
                "lang" => NULL,
202
                "value" => "on",
203
                "level" => "FED",
204
                "row" => 0,
205
                "flag" => NULL);
206
        }
207
208
        $this->idpListActive = [];
209
        $this->idpListAll = [];
210
    }
211
212
    /**
213
     * Creates a new IdP inside the federation.
214
     * 
215
     * @param string $type          type of institution - IdP, SP or IdPSP
216
     * @param string $ownerId       Persistent identifier of the user for whom this IdP is created (first administrator)
217
     * @param string $level         Privilege level of the first administrator (was he blessed by a federation admin or a peer?)
218
     * @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)
219
     * @param string $bestnameguess name of the IdP, if already known, in the best-match language
220
     * @return int identifier of the new IdP
221
     */
222
    public function newIdP($type, $ownerId, $level, $mail = NULL, $bestnameguess = NULL) {
223
        $this->databaseHandle->exec("INSERT INTO institution (country, type) VALUES('$this->tld', '$type')");
224
        $identifier = $this->databaseHandle->lastID();
225
226
        if ($identifier == 0 || !$this->loggerInstance->writeAudit($ownerId, "NEW", "IdP $identifier")) {
227
            $text = "<p>Could not create a new " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_inst'] . "!</p>";
228
            echo $text;
229
            throw new Exception($text);
230
        }
231
232
        if ($ownerId != "PENDING") {
233
            if ($mail === NULL) {
234
                throw new Exception("New IdPs in a federation need a mail address UNLESS created by API without OwnerId");
235
            }
236
            $this->databaseHandle->exec("INSERT INTO ownership (user_id,institution_id, blesslevel, orig_mail) VALUES(?,?,?,?)", "siss", $ownerId, $identifier, $level, $mail);
237
        }
238
        if ($bestnameguess === NULL) {
239
            $bestnameguess = "(no name yet, identifier $identifier)";
240
        }
241
        $admins = $this->listFederationAdmins();
242
243
        // notify the fed admins...
244
245
        foreach ($admins as $id) {
246
            $user = new User($id);
247
            /// arguments are: 1. nomenclature for "institution"
248
            //                 2. IdP name; 
249
            ///                3. consortium name (e.g. eduroam); 
250
            ///                4. federation shortname, e.g. "LU"; 
251
            ///                5. product name (e.g. eduroam CAT); 
252
            ///                6. product long name (e.g. eduroam Configuration Assistant Tool)
253
            $message = sprintf(_("Hi,
254
255
the invitation for the new %s %s in your %s federation %s has been used and the IdP was created in %s.
256
257
We thought you might want to know.
258
259
Best regards,
260
261
%s"), common\Entity::$nomenclature_inst, $bestnameguess, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], strtoupper($this->tld), CONFIG['APPEARANCE']['productname'], CONFIG['APPEARANCE']['productname_long']);
262
            $retval = $user->sendMailToUser(sprintf(_("%s in your federation was created"), common\Entity::$nomenclature_inst), $message);
263
            if ($retval === FALSE) {
264
                $this->loggerInstance->debug(2, "Mail to federation admin was NOT sent!\n");
265
            }
266
        }
267
268
        return $identifier;
269
    }
270
271
    private $idpListAll;
272
    private $idpListActive;
273
274
    /**
275
     * Lists all Identity Providers in this federation
276
     *
277
     * @param int $activeOnly if set to non-zero will list only those institutions which have some valid profiles defined.
278
     * @return array (Array of IdP instances)
279
     *
280
     */
281
    public function listIdentityProviders($activeOnly = 0) {
282
        // maybe we did this exercise before?
283
        if ($activeOnly != 0 && count($this->idpListActive) > 0) {
284
            return $this->idpListActive;
285
        }
286
        if ($activeOnly == 0 && count($this->idpListAll) > 0) {
287
            return $this->idpListAll;
288
        }
289
        // default query is:
290
        $allIDPs = $this->databaseHandle->exec("SELECT inst_id FROM institution
291
               WHERE country = '$this->tld' ORDER BY inst_id");
292
        // the one for activeOnly is much more complex:
293
        if ($activeOnly) {
294
            $allIDPs = $this->databaseHandle->exec("SELECT distinct institution.inst_id AS inst_id
295
               FROM institution
296
               JOIN profile ON institution.inst_id = profile.inst_id
297
               WHERE institution.country = '$this->tld' 
298
               AND profile.showtime = 1
299
               ORDER BY inst_id");
300
        }
301
302
        $returnarray = [];
303
        // SELECT -> resource, not boolean
304
        while ($idpQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allIDPs)) {
305
            $idp = new IdP($idpQuery->inst_id);
306
            $name = $idp->name;
307
            $idpInfo = ['entityID' => $idp->identifier,
308
                'title' => $name,
309
                'country' => strtoupper($idp->federation),
310
                'instance' => $idp];
311
            $returnarray[$idp->identifier] = $idpInfo;
312
        }
313
        if ($activeOnly != 0) { // we're only doing this once.
314
            $this->idpListActive = $returnarray;
315
        } else {
316
            $this->idpListAll = $returnarray;
317
        }
318
        return $returnarray;
319
    }
320
321
    /**
322
     * returns an array with information about the authorised administrators of the federation
323
     * 
324
     * @return array list of the admins of this federation
325
     */
326
    public function listFederationAdmins() {
327
        $returnarray = [];
328
        $query = "SELECT user_id FROM user_options WHERE option_name = 'user:fedadmin' AND option_value = ?";
329
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
330
            $query = "SELECT eptid as user_id FROM view_admin WHERE role = 'fedadmin' AND realm = ?";
331
        }
332
        $userHandle = DBConnection::handle("USER"); // we need something from the USER database for a change
333
        $upperFed = strtoupper($this->tld);
334
        // SELECT -> resource, not boolean
335
        $admins = $userHandle->exec($query, "s", $upperFed);
336
337
        while ($fedAdminQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
338
            $returnarray[] = $fedAdminQuery->user_id;
339
        }
340
        return $returnarray;
341
    }
342
343
    /**
344
     * cross-checks in the EXTERNAL customer DB which institutions exist there for the federations
345
     * 
346
     * @param bool $unmappedOnly if set to TRUE, only returns those which do not have a known mapping to our internally known institutions
347
     * @return array
348
     */
349
    public function listExternalEntities($unmappedOnly) {
350
        $allExternals = [];
351
        $usedarray = [];
352
        if ($unmappedOnly) { // find out which entities are already mapped
353
            $syncstate = IdP::EXTERNAL_DB_SYNCSTATE_SYNCED;
354
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution 
355
                                                                                                     WHERE external_db_id IS NOT NULL 
356
                                                                                                     AND external_db_syncstate = ?", "i", $syncstate);
357
            $pendingInvite = $this->databaseHandle->exec("SELECT DISTINCT external_db_uniquehandle FROM invitations 
358
                                                                                                      WHERE external_db_uniquehandle IS NOT NULL 
359
                                                                                                      AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) 
360
                                                                                                      AND used = 0");
361
            // SELECT -> resource, no boolean
362
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
363
                $usedarray[] = $alreadyUsedQuery->external_db_id;
364
            }
365
            // SELECT -> resource, no boolean
366
            while ($pendingInviteQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $pendingInvite)) {
367
                if (!in_array($pendingInviteQuery->external_db_uniquehandle, $usedarray)) {
368
                    $usedarray[] = $pendingInviteQuery->external_db_uniquehandle;
369
                }
370
            }
371
        }
372
373
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
374
            $eduroamDb = new ExternalEduroamDBData();
375
            $allExternals = $eduroamDb->listExternalEntities();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $allExternals is correct as $eduroamDb->listExternalEntities() targeting core\ExternalEduroamDBData::listExternalEntities() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
376
        }
377
        foreach ($allExternals as $oneExternal) {
378
            if (!in_array($oneExternal["ID"], $usedarray)) {
379
                $returnarray[] = $oneExternal;
380
            }
381
        }
382
        return $returnarray;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnarray does not seem to be defined for all execution paths leading up to this point.
Loading history...
383
    }
384
385
    const UNKNOWN_IDP = -1;
386
    const AMBIGUOUS_IDP = -2;
387
388
    /**
389
     * for a MySQL list of institutions, find an institution or find out that
390
     * there is no single best match
391
     * 
392
     * @param \mysqli_result $dbResult the query object to work with
393
     * @param string         $country  used to return the country of the inst, if can be found out
394
     * @return int the identifier of the inst, or one of the special return values if unsuccessful
395
     */
396
    private static function findCandidates(\mysqli_result $dbResult, &$country) {
397
        $retArray = [];
398
        while ($row = mysqli_fetch_object($dbResult)) {
399
            if (!in_array($row->id, $retArray)) {
400
                $retArray[] = $row->id;
401
                $country = strtoupper($row->country);
402
            }
403
        }
404
        if (count($retArray) <= 0) {
405
            return Federation::UNKNOWN_IDP;
406
        }
407
        if (count($retArray) > 1) {
408
            return Federation::AMBIGUOUS_IDP;
409
        }
410
411
        return array_pop($retArray);
412
    }
413
414
    /**
415
     * If we are running diagnostics, our input from the user is the realm. We
416
     * need to find out which IdP this realm belongs to.
417
     * @param string $realm the realm to search for
418
     * @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
419
     */
420
    public static function determineIdPIdByRealm($realm) {
421
        $country = NULL;
422
        $candidatesExternalDb = Federation::UNKNOWN_IDP;
423
        $dbHandle = DBConnection::handle("INST");
424
        $realmSearchStringCat = "%@$realm";
425
        $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);
426
        // this is a SELECT returning a resource, not a boolean
427
        $candidatesCat = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateCatQuery, $country);
428
429
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED        
430
            $externalHandle = DBConnection::handle("EXTERNAL");
431
            $realmSearchStringDb1 = "$realm";
432
            $realmSearchStringDb2 = "%,$realm";
433
            $realmSearchStringDb3 = "$realm,%";
434
            $realmSearchStringDb4 = "%,$realm,%";
435
            $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);
436
            // SELECT -> resource, not boolean
437
            $candidatesExternalDb = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateExternalQuery, $country);
438
        }
439
440
        return ["CAT" => $candidatesCat, "EXTERNAL" => $candidatesExternalDb, "FEDERATION" => $country];
441
    }
442
443
    /**
444
     * helper function to sort institutions by their name
445
     * @param array $a an array with institution a's information
446
     * @param array $b an array with institution b's information
447
     * @return int the comparison result
448
     */
449
    private function usortInstitution($a, $b) {
450
        return strcasecmp($a["name"], $b["name"]);
451
    }
452
453
}
454