Passed
Push — master ( 5c87ed...e4bb53 )
by Stefan
04:05
created

Federation::findCandidates()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
c 0
b 0
f 0
rs 8.8571
cc 5
eloc 10
nc 9
nop 2
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) {
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>";
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
203
            echo $text;
204
            throw new Exception($text);
205
        }
206
207
        if ($ownerId != "PENDING") {
208
            $this->databaseHandle->exec("INSERT INTO ownership (user_id,institution_id, blesslevel, orig_mail) VALUES(?,?,?,?)", "siss", $ownerId, $identifier, $level, $mail);
209
        }
210
        return $identifier;
211
    }
212
213
    /**
214
     * Lists all Identity Providers in this federation
215
     *
216
     * @param int $activeOnly if set to non-zero will list only those institutions which have some valid profiles defined.
217
     * @return array (Array of IdP instances)
218
     *
219
     */
220
    public function listIdentityProviders($activeOnly = 0) {
221
        // default query is:
222
        $allIDPs = $this->databaseHandle->exec("SELECT inst_id FROM institution
223
               WHERE country = '$this->tld' ORDER BY inst_id");
224
        // the one for activeOnly is much more complex:
225
        if ($activeOnly) {
226
            $allIDPs = $this->databaseHandle->exec("SELECT distinct institution.inst_id AS inst_id
227
               FROM institution
228
               JOIN profile ON institution.inst_id = profile.inst_id
229
               WHERE institution.country = '$this->tld' 
230
               AND profile.showtime = 1
231
               ORDER BY inst_id");
232
        }
233
234
        $returnarray = [];
235
        // SELECT -> resource, not boolean
236
        while ($idpQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allIDPs)) {
237
            $idp = new IdP($idpQuery->inst_id);
238
            $name = $idp->name;
239
            $idpInfo = ['entityID' => $idp->identifier,
240
                'title' => $name,
241
                'country' => strtoupper($idp->federation),
242
                'instance' => $idp];
243
            $returnarray[$idp->identifier] = $idpInfo;
244
        }
245
        return $returnarray;
246
    }
247
248
    /**
249
     * returns an array with information about the authorised administrators of the federation
250
     * 
251
     * @return array
252
     */
253
    public function listFederationAdmins() {
254
        $returnarray = [];
255
        $query = "SELECT user_id FROM user_options WHERE option_name = 'user:fedadmin' AND option_value = ?";
256
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
257
            $query = "SELECT eptid as user_id FROM view_admin WHERE role = 'fedadmin' AND realm = ?";
258
        }
259
        $userHandle = DBConnection::handle("USER"); // we need something from the USER database for a change
260
        $upperFed = strtoupper($this->tld);
261
        // SELECT -> resource, not boolean
262
        $admins = $userHandle->exec($query, "s", $upperFed);
263
264
        while ($fedAdminQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
265
            $returnarray[] = $fedAdminQuery->user_id;
266
        }
267
        return $returnarray;
268
    }
269
270
    /**
271
     * cross-checks in the EXTERNAL customer DB which institutions exist there for the federations
272
     * 
273
     * @param bool $unmappedOnly if set to TRUE, only returns those which do not have a known mapping to our internally known institutions
274
     * @return array
275
     */
276
    public function listExternalEntities($unmappedOnly) {
277
        $returnarray = [];
278
279
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
280
            $usedarray = [];
281
            $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 = ?";
282
283
284
            $externalHandle = DBConnection::handle("EXTERNAL");
285
            $externals = $externalHandle->exec($query, "s", $this->tld);
286
            $syncstate = IdP::EXTERNAL_DB_SYNCSTATE_SYNCED;
287
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution 
288
                                                                                                     WHERE external_db_id IS NOT NULL 
289
                                                                                                     AND external_db_syncstate = ?", "i", $syncstate);
290
            $pendingInvite = $this->databaseHandle->exec("SELECT DISTINCT external_db_uniquehandle FROM invitations 
291
                                                                                                      WHERE external_db_uniquehandle IS NOT NULL 
292
                                                                                                      AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) 
293
                                                                                                      AND used = 0");
294
            // SELECT -> resource, no boolean
295
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
296
                $usedarray[] = $alreadyUsedQuery->external_db_id;
297
            }
298
            // SELECT -> resource, no boolean
299
            while ($pendingInviteQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $pendingInvite)) {
300
                if (!in_array($pendingInviteQuery->external_db_uniquehandle, $usedarray)) {
301
                    $usedarray[] = $pendingInviteQuery->external_db_uniquehandle;
302
                }
303
            }
304
            // was a SELECT query, so a resource and not a boolean
305
            while ($externalQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $externals)) {
306
                if (($unmappedOnly === TRUE) && (in_array($externalQuery->id, $usedarray))) {
307
                    continue;
308
                }
309
                $names = explode('#', $externalQuery->collapsed_name);
310
                // trim name list to current best language match
311
                $availableLanguages = [];
312
                foreach ($names as $name) {
313
                    $thislang = explode(': ', $name, 2);
314
                    $availableLanguages[$thislang[0]] = $thislang[1];
315
                }
316
                if (array_key_exists($this->languageInstance->getLang(), $availableLanguages)) {
317
                    $thelangauge = $availableLanguages[$this->languageInstance->getLang()];
318
                } else if (array_key_exists("en", $availableLanguages)) {
319
                    $thelangauge = $availableLanguages["en"];
320
                } else { // whatever. Pick one out of the list
321
                    $thelangauge = array_pop($availableLanguages);
322
                }
323
                $contacts = explode('#', $externalQuery->collapsed_contact);
324
325
326
                $mailnames = "";
327
                foreach ($contacts as $contact) {
328
                    $matches = [];
329
                    preg_match("/^n: (.*), e: (.*), p: .*$/", $contact, $matches);
330
                    if ($matches[2] != "") {
331
                        if ($mailnames != "") {
332
                            $mailnames .= ", ";
333
                        }
334
                        // extracting real names is nice, but the <> notation
335
                        // really gets screwed up on POSTs and HTML safety
336
                        // so better not do this; use only mail addresses
337
                        $mailnames .= $matches[2];
338
                    }
339
                }
340
                $returnarray[] = ["ID" => $externalQuery->id, "name" => $thelangauge, "contactlist" => $mailnames, "country" => $externalQuery->country, "realmlist" => $externalQuery->realmlist];
341
            }
342
            usort($returnarray, array($this, "usortInstitution"));
343
        }
344
        return $returnarray;
345
    }
346
347
    const UNKNOWN_IDP = -1;
348
    const AMBIGUOUS_IDP = -2;
349
350
    /**
351
     * for a MySQL list of institutions, find an institution or find out that
352
     * there is no single best match
353
     * 
354
     * @param \mysqli_result $dbResult
355
     * @param string $country used to return the country of the inst, if can be found out
356
     * @return int the identifier of the inst, or one of the special return values if unsuccessful
357
     */
358
    private static function findCandidates(\mysqli_result $dbResult, &$country) {
359
        $retArray = [];
360
        while ($row = mysqli_fetch_object($dbResult)) {
361
            if (!in_array($row->id, $retArray)) {
362
                $retArray[] = $row->id;
363
                $country = strtoupper($row->country);
364
            }
365
        }
366
        if (count($retArray) <= 0) {
367
            return Federation::UNKNOWN_IDP;
368
        }
369
        if (count($retArray) > 1) {
370
            return Federation::AMBIGUOUS_IDP;
371
        }
372
373
        return array_pop($retArray);
374
    }
375
376
    /**
377
     * If we are running diagnostics, our input from the user is the realm. We
378
     * need to find out which IdP this realm belongs to.
379
     * @param string $realm the realm to search for
380
     * @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
381
     */
382
    public static function determineIdPIdByRealm($realm) {
383
        $country = NULL;
384
        $candidatesExternalDb = Federation::UNKNOWN_IDP;
385
        $dbHandle = DBConnection::handle("INST");
386
        $realmSearchStringCat = "%@$realm";
387
        $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);
388
        // this is a SELECT returning a resource, not a boolean
389
        $candidatesCat = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateCatQuery, $country);
390
391
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED        
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
392
            $externalHandle = DBConnection::handle("EXTERNAL");
393
            $realmSearchStringDb1 = "$realm";
394
            $realmSearchStringDb2 = "%,$realm";
395
            $realmSearchStringDb3 = "$realm,%";
396
            $realmSearchStringDb4 = "%,$realm,%";
397
            $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);
398
            // SELECT -> resource, not boolean
399
            $candidatesExternalDb = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateExternalQuery, $country);
400
        }
401
402
        return ["CAT" => $candidatesCat, "EXTERNAL" => $candidatesExternalDb, "FEDERATION" => $country];
403
    }
404
405
    /**
406
     * helper function to sort institutions by their name
407
     * @param array $a an array with institution a's information
408
     * @param array $b an array with institution b's information
409
     * @return int the comparison result
410
     */
411
    private function usortInstitution($a, $b) {
412
        return strcasecmp($a["name"], $b["name"]);
413
    }
414
415
}
416