Passed
Push — master ( 091442...05ae42 )
by Stefan
06:52
created

Federation::__construct()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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