Test Failed
Branch release_2_0 (0369fe)
by Stefan
08:46
created

Federation::newIdP()   B

Complexity

Conditions 8
Paths 14

Size

Total Lines 47
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 47
rs 8.4444
c 0
b 0
f 0
cc 8
nc 14
nop 4
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
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $bestnameguess should have a doc-comment as per coding-style.
Loading history...
213
     * Creates a new IdP inside the federation.
214
     * 
215
     * @param string $ownerId Persistent identifier of the user for whom this IdP is created (first administrator)
216
     * @param string $level   Privilege level of the first administrator (was he blessed by a federation admin or a peer?)
217
     * @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)
218
     * @return int identifier of the new IdP
219
     */
220
    public function newIdP($ownerId, $level, $mail = NULL, $bestnameguess = NULL) {
221
        $this->databaseHandle->exec("INSERT INTO institution (country) VALUES('$this->tld')");
222
        $identifier = $this->databaseHandle->lastID();
223
224
        if ($identifier == 0 || !$this->loggerInstance->writeAudit($ownerId, "NEW", "IdP $identifier")) {
225
            $text = "<p>Could not create a new " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_inst'] . "!</p>";
226
            echo $text;
227
            throw new Exception($text);
228
        }
229
230
        if ($ownerId != "PENDING") {
231
            if ($mail === NULL) {
232
                throw new Exception("New IdPs in a federation need a mail address UNLESS created by API without OwnerId");
233
            }
234
            $this->databaseHandle->exec("INSERT INTO ownership (user_id,institution_id, blesslevel, orig_mail) VALUES(?,?,?,?)", "siss", $ownerId, $identifier, $level, $mail);
235
        }
236
        if ($bestnameguess == NULL) {
237
            $bestnameguess = "(no name yet, identifier $identifier)";
238
        }
239
        $admins = $this->listFederationAdmins();
240
241
        // notify the fed admins...
242
243
        foreach ($admins as $id) {
244
            $user = new User($id);
245
            /// arguments are: 1. nomenclature for "institution"
246
            //                 2. IdP name; 
247
            ///                3. consortium name (e.g. eduroam); 
248
            ///                4. federation shortname, e.g. "LU"; 
249
            ///                5. product name (e.g. eduroam CAT); 
250
            ///                6. product long name (e.g. eduroam Configuration Assistant Tool)
251
            $message = sprintf(_("Hi,
252
253
the invitation for the new %s %s in your %s federation %s has been used and the IdP was created in %s.
254
255
We thought you might want to know.
256
257
Best regards,
258
259
%s"), common\Entity::$nomenclature_inst, $bestnameguess, CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'], strtoupper($this->tld), CONFIG['APPEARANCE']['productname'], CONFIG['APPEARANCE']['productname_long']);
260
            $retval = $user->sendMailToUser(sprintf(_("%s in your federation was created"), common\Entity::$nomenclature_inst), $message);
261
            if ($retval === FALSE) {
262
                $this->loggerInstance->debug(2, "Mail to federation admin was NOT sent!\n");
263
            }
264
        }
265
266
        return $identifier;
267
    }
268
269
    private $idpListAll;
270
    private $idpListActive;
271
272
    /**
273
     * Lists all Identity Providers in this federation
274
     *
275
     * @param int $activeOnly if set to non-zero will list only those institutions which have some valid profiles defined.
276
     * @return array (Array of IdP instances)
277
     *
278
     */
279
    public function listIdentityProviders($activeOnly = 0) {
280
        // maybe we did this exercise before?
281
        if ($activeOnly != 0 && count($this->idpListActive) > 0) {
282
            return $this->idpListActive;
283
        }
284
        if ($activeOnly == 0 && count($this->idpListAll) > 0) {
285
            return $this->idpListAll;
286
        }
287
        // default query is:
288
        $allIDPs = $this->databaseHandle->exec("SELECT inst_id FROM institution
289
               WHERE country = '$this->tld' ORDER BY inst_id");
290
        // the one for activeOnly is much more complex:
291
        if ($activeOnly) {
292
            $allIDPs = $this->databaseHandle->exec("SELECT distinct institution.inst_id AS inst_id
293
               FROM institution
294
               JOIN profile ON institution.inst_id = profile.inst_id
295
               WHERE institution.country = '$this->tld' 
296
               AND profile.showtime = 1
297
               ORDER BY inst_id");
298
        }
299
300
        $returnarray = [];
301
        // SELECT -> resource, not boolean
302
        while ($idpQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $allIDPs)) {
303
            $idp = new IdP($idpQuery->inst_id);
304
            $name = $idp->name;
305
            $idpInfo = ['entityID' => $idp->identifier,
306
                'title' => $name,
307
                'country' => strtoupper($idp->federation),
308
                'instance' => $idp];
309
            $returnarray[$idp->identifier] = $idpInfo;
310
        }
311
        if ($activeOnly != 0) { // we're only doing this once.
312
            $this->idpListActive = $returnarray;
313
        } else {
314
            $this->idpListAll = $returnarray;
315
        }
316
        return $returnarray;
317
    }
318
319
    /**
320
     * returns an array with information about the authorised administrators of the federation
321
     * 
322
     * @return array list of the admins of this federation
323
     */
324
    public function listFederationAdmins() {
325
        $returnarray = [];
326
        $query = "SELECT user_id FROM user_options WHERE option_name = 'user:fedadmin' AND option_value = ?";
327
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
328
            $query = "SELECT eptid as user_id FROM view_admin WHERE role = 'fedadmin' AND realm = ?";
329
        }
330
        $userHandle = DBConnection::handle("USER"); // we need something from the USER database for a change
331
        $upperFed = strtoupper($this->tld);
332
        // SELECT -> resource, not boolean
333
        $admins = $userHandle->exec($query, "s", $upperFed);
334
335
        while ($fedAdminQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $admins)) {
336
            $returnarray[] = $fedAdminQuery->user_id;
337
        }
338
        return $returnarray;
339
    }
340
341
    /**
342
     * cross-checks in the EXTERNAL customer DB which institutions exist there for the federations
343
     * 
344
     * @param bool $unmappedOnly if set to TRUE, only returns those which do not have a known mapping to our internally known institutions
345
     * @return array
346
     */
347
    public function listExternalEntities($unmappedOnly) {
348
        $returnarray = [];
349
350
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
351
            $usedarray = [];
352
            $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 = ?";
353
354
355
            $externalHandle = DBConnection::handle("EXTERNAL");
356
            $externals = $externalHandle->exec($query, "s", $this->tld);
357
            $syncstate = IdP::EXTERNAL_DB_SYNCSTATE_SYNCED;
358
            $alreadyUsed = $this->databaseHandle->exec("SELECT DISTINCT external_db_id FROM institution 
359
                                                                                                     WHERE external_db_id IS NOT NULL 
360
                                                                                                     AND external_db_syncstate = ?", "i", $syncstate);
361
            $pendingInvite = $this->databaseHandle->exec("SELECT DISTINCT external_db_uniquehandle FROM invitations 
362
                                                                                                      WHERE external_db_uniquehandle IS NOT NULL 
363
                                                                                                      AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) 
364
                                                                                                      AND used = 0");
365
            // SELECT -> resource, no boolean
366
            while ($alreadyUsedQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $alreadyUsed)) {
367
                $usedarray[] = $alreadyUsedQuery->external_db_id;
368
            }
369
            // SELECT -> resource, no boolean
370
            while ($pendingInviteQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $pendingInvite)) {
371
                if (!in_array($pendingInviteQuery->external_db_uniquehandle, $usedarray)) {
372
                    $usedarray[] = $pendingInviteQuery->external_db_uniquehandle;
373
                }
374
            }
375
            // was a SELECT query, so a resource and not a boolean
376
            while ($externalQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $externals)) {
377
                if (($unmappedOnly === TRUE) && (in_array($externalQuery->id, $usedarray))) {
378
                    continue;
379
                }
380
                $names = explode('#', $externalQuery->collapsed_name);
381
                // trim name list to current best language match
382
                $availableLanguages = [];
383
                foreach ($names as $name) {
384
                    $thislang = explode(': ', $name, 2);
385
                    $availableLanguages[$thislang[0]] = $thislang[1];
386
                }
387
                if (array_key_exists($this->languageInstance->getLang(), $availableLanguages)) {
388
                    $thelangauge = $availableLanguages[$this->languageInstance->getLang()];
389
                } else if (array_key_exists("en", $availableLanguages)) {
390
                    $thelangauge = $availableLanguages["en"];
391
                } else { // whatever. Pick one out of the list
392
                    $thelangauge = array_pop($availableLanguages);
393
                }
394
                $contacts = explode('#', $externalQuery->collapsed_contact);
395
396
397
                $mailnames = "";
398
                foreach ($contacts as $contact) {
399
                    $matches = [];
400
                    preg_match("/^n: (.*), e: (.*), p: .*$/", $contact, $matches);
401
                    if ($matches[2] != "") {
402
                        if ($mailnames != "") {
403
                            $mailnames .= ", ";
404
                        }
405
                        // extracting real names is nice, but the <> notation
406
                        // really gets screwed up on POSTs and HTML safety
407
                        // so better not do this; use only mail addresses
408
                        $mailnames .= $matches[2];
409
                    }
410
                }
411
                $returnarray[] = ["ID" => $externalQuery->id, "name" => $thelangauge, "contactlist" => $mailnames, "country" => $externalQuery->country, "realmlist" => $externalQuery->realmlist];
412
            }
413
            usort($returnarray, array($this, "usortInstitution"));
414
        }
415
        return $returnarray;
416
    }
417
418
    const UNKNOWN_IDP = -1;
419
    const AMBIGUOUS_IDP = -2;
420
421
    /**
422
     * for a MySQL list of institutions, find an institution or find out that
423
     * there is no single best match
424
     * 
425
     * @param \mysqli_result $dbResult the query object to work with
426
     * @param string         $country  used to return the country of the inst, if can be found out
427
     * @return int the identifier of the inst, or one of the special return values if unsuccessful
428
     */
429
    private static function findCandidates(\mysqli_result $dbResult, &$country) {
430
        $retArray = [];
431
        while ($row = mysqli_fetch_object($dbResult)) {
432
            if (!in_array($row->id, $retArray)) {
433
                $retArray[] = $row->id;
434
                $country = strtoupper($row->country);
435
            }
436
        }
437
        if (count($retArray) <= 0) {
438
            return Federation::UNKNOWN_IDP;
439
        }
440
        if (count($retArray) > 1) {
441
            return Federation::AMBIGUOUS_IDP;
442
        }
443
444
        return array_pop($retArray);
445
    }
446
447
    /**
448
     * If we are running diagnostics, our input from the user is the realm. We
449
     * need to find out which IdP this realm belongs to.
450
     * @param string $realm the realm to search for
451
     * @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
452
     */
453
    public static function determineIdPIdByRealm($realm) {
454
        $country = NULL;
455
        $candidatesExternalDb = Federation::UNKNOWN_IDP;
456
        $dbHandle = DBConnection::handle("INST");
457
        $realmSearchStringCat = "%@$realm";
458
        $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);
459
        // this is a SELECT returning a resource, not a boolean
460
        $candidatesCat = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateCatQuery, $country);
461
462
        if (CONFIG_CONFASSISTANT['CONSORTIUM']['name'] == "eduroam" && isset(CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo']) && CONFIG_CONFASSISTANT['CONSORTIUM']['deployment-voodoo'] == "Operations Team") { // SW: APPROVED        
463
            $externalHandle = DBConnection::handle("EXTERNAL");
464
            $realmSearchStringDb1 = "$realm";
465
            $realmSearchStringDb2 = "%,$realm";
466
            $realmSearchStringDb3 = "$realm,%";
467
            $realmSearchStringDb4 = "%,$realm,%";
468
            $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);
469
            // SELECT -> resource, not boolean
470
            $candidatesExternalDb = Federation::findCandidates(/** @scrutinizer ignore-type */ $candidateExternalQuery, $country);
471
        }
472
473
        return ["CAT" => $candidatesCat, "EXTERNAL" => $candidatesExternalDb, "FEDERATION" => $country];
474
    }
475
476
    /**
477
     * helper function to sort institutions by their name
478
     * @param array $a an array with institution a's information
479
     * @param array $b an array with institution b's information
480
     * @return int the comparison result
481
     */
482
    private function usortInstitution($a, $b) {
483
        return strcasecmp($a["name"], $b["name"]);
484
    }
485
486
}
487