Passed
Push — master ( 17bffe...10bee5 )
by Stefan
06:13
created

Federation   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 361
Duplicated Lines 3.32 %

Importance

Changes 0
Metric Value
wmc 53
dl 12
loc 361
rs 7.4757
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
B downloadStatsCore() 0 35 5
C listExternalEntities() 0 69 16
A updateFreshness() 0 1 1
B findCandidates() 0 16 5
A determineIdPIdByRealm() 0 21 4
A usortInstitution() 0 2 1
B listFederationAdmins() 0 15 5
B __construct() 0 32 2
C downloadStats() 12 31 7
B listIdentityProviders() 0 26 3
A newIdP() 0 14 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
     * retrieve the statistics from the database in an internal array representation
55
     * 
56
     * @return array
57
     */
58
    private function downloadStatsCore() {
59
        $grossAdmin = 0;
60
        $grossUser = 0;
61
        $grossSilverbullet = 0;
62
        $dataArray = [];
63
        // first, find out which profiles belong to this federation
64
        $cohesionQuery = "SELECT profile_id FROM profile, institution WHERE profile.inst_id = institution.inst_id AND institution.country = ?";
65
        $profilesList = $this->databaseHandle->exec($cohesionQuery, "s", $this->identifier);
66
        $profilesArray = [];
67
        // SELECT -> resource is returned, no boolean
68
        while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $profilesList)) {
69
            $profilesArray[] = $result->profile_id;
70
        }
71
        foreach (\devices\Devices::listDevices() as $index => $deviceArray) {
72
            $countDevice = [];
73
            $countDevice['ADMIN'] = 0;
74
            $countDevice['SILVERBULLET'] = 0;
75
            $countDevice['USER'] = 0;
76
            foreach ($profilesArray as $profileNumber) {
77
                $deviceQuery = "SELECT downloads_admin, downloads_silverbullet, downloads_user FROM downloads WHERE device_id = ? AND profile_id = ?";
78
                $statsList = $this->frontendHandle->exec($deviceQuery, "si", $index, $profileNumber);
79
                // SELECT -> resource, no boolean
80
                while ($queryResult = mysqli_fetch_object(/** @scrutinizer ignore-type */ $statsList)) {
81
                    $countDevice['ADMIN'] = $countDevice['ADMIN'] + $queryResult->downloads_admin;
82
                    $countDevice['SILVERBULLET'] = $countDevice['SILVERBULLET'] + $queryResult->downloads_silverbullet;
83
                    $countDevice['USER'] = $countDevice['USER'] + $queryResult->downloads_user;
84
                    $grossAdmin = $grossAdmin + $queryResult->downloads_admin;
85
                    $grossSilverbullet = $grossSilverbullet + $queryResult->downloads_silverbullet;
86
                    $grossUser = $grossUser + $queryResult->downloads_user;
87
                }
88
                $dataArray[$deviceArray['display']] = ["ADMIN" => $countDevice['ADMIN'], "SILVERBULLET" => $countDevice['SILVERBULLET'], "USER" => $countDevice['USER']];
89
            }
90
        }
91
        $dataArray["TOTAL"] = ["ADMIN" => $grossAdmin, "SILVERBULLET" => $grossSilverbullet, "USER" => $grossUser];
92
        return $dataArray;
93
    }
94
95
    /**
96
     * NOOP on Federations, but have to override the abstract parent method
97
     */
98
    public function updateFreshness() {
99
        // Federation is always fresh
100
    }
101
102
    /**
103
     * gets the download statistics for the federation
104
     * @param string $format either as an html *table* or *XML*
105
     * @return string
106
     */
107
    public function downloadStats($format) {
108
        $data = $this->downloadStatsCore();
109
        $retstring = "";
110
111
        switch ($format) {
112
            case "table":
113 View Code Duplication
                foreach ($data as $device => $numbers) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
114
                    if ($device == "TOTAL") {
115
                        continue;
116
                    }
117
                    $retstring .= "<tr><td>$device</td><td>" . $numbers['ADMIN'] . "</td><td>" . $numbers['SILVERBULLET'] . "</td><td>" . $numbers['USER'] . "</td></tr>";
118
                }
119
                $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>";
120
                break;
121
            case "XML":
122
                // the calls to date() operate on current date, so there is no chance for a FALSE to be returned. Silencing scrutinizer.
123
                $retstring .= "<federation id='$this->identifier' ts='" . /** @scrutinizer ignore-type */ date("Y-m-d") . "T" . /** @scrutinizer ignore-type */ date("H:i:s") . "'>\n";
124 View Code Duplication
                foreach ($data as $device => $numbers) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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