Test Failed
Push — master ( f4077b...8d6a7b )
by Tomasz
12:00
created

dissectCollapsedInstitutionRealms()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 3
nc 2
nop 1
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 ExternalEduroamDBData class. It contains methods for
25
 * querying the external database.
26
 *
27
 * @author Stefan Winter <[email protected]>
28
 * @author Maja Górecka-Wolniewicz <[email protected]>
29
 * @author Tomasz Wolniewicz <[email protected]>
30
 *
31
 * @package Developer
32
 *
33
 */
34
35
namespace core;
36
37
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
39
/**
40
 * This class interacts with the external DB to fetch operational data for
41
 * read-only purposes.
42
 * 
43
 * @author Stefan Winter <[email protected]>
44
 * @author Maja Górecka-Wolniewicz <[email protected]>
45
 * @author Tomasz Wolniewicz <[email protected]>
46
 *
47
 * @license see LICENSE file in root directory
48
 *
49
 * @package Developer
50
 */
51
class ExternalEduroamDBData extends common\Entity implements ExternalLinkInterface {
52
53
    /**
54
     * List of all service providers. Fetched only once by allServiceProviders()
55
     * and then stored in this property for efficiency
56
     * 
57
     * @var array
58
     */
59
    private $SPList = [];
60
61
    /**
62
     * total number of hotspots, cached here for efficiency
63
     * 
64
     * @var int
65
     */
66
    private $counter = -1;
67
68
    /**
69
     * our handle to the external DB
70
     * 
71
     * @var DBConnection
72
     */
73
    private $db;
74
    
75
    /**
76
     * our handle to the local DB
77
     * 
78
     * @var DBConnection
79
     */
80
    private $localDb;
81
    
82
    
83
    /**
84
     * constructor, gives us access to the DB handle we need for queries
85
     */
86
    public function __construct() {
87
        parent::__construct();
88
        $connHandle = DBConnection::handle("EXTERNAL");
89
        if (!$connHandle instanceof DBConnection) {
90
            throw new Exception("Frontend DB is never an array, always a single DB object.");
91
        }
92
        $this->db = $connHandle;
93
        $localConnHandle = DBConnection::handle("INST");
94
        if (!$localConnHandle instanceof DBConnection) {
95
            throw new Exception("Frontend DB is never an array, always a single DB object.");
96
        }
97
        $this->localDb = $localConnHandle;
98
    }
99
100
    /**
101
     * eduroam DB delivers a string with all name variants mangled in one. Pry
102
     * it apart.
103
     * 
104
     * @param string $nameRaw the string with all name variants coerced into one
105
     * @return array language/name pair
106
     * @throws Exception
107
     */
108
    private function splitNames($nameRaw) {
109
        $variants = explode('#', $nameRaw);
110
        $submatches = [];
111
        $returnArray = [];
112
        foreach ($variants as $oneVariant) {
113
            if ($oneVariant == NULL) {
114
                continue;
115
            }
116
            if (!preg_match('/^(..):\ (.*)/', $oneVariant, $submatches) || !isset($submatches[2])) {
117
                $this->loggerInstance->debug(2, "[$nameRaw] We expect 'xx: bla but found '$oneVariant'.");
118
                continue;
119
            }
120
            $returnArray[$submatches[1]] = $submatches[2];
121
        }
122
        return $returnArray;
123
    }
124
125
    /**
126
     * retrieves the list of all service providers from the eduroam database
127
     * 
128
     * @return array list of providers
129
     */
130
    public function listAllServiceProviders() {
131
        if (count($this->SPList) == 0) {
132
            $query = $this->db->exec("SELECT country, inst_name, sp_location FROM view_active_SP_location_eduroamdb");
133
            while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $query)) {
134
                $this->SPList[] = ["country" => $iterator->country, "instnames" => $this->splitNames($iterator->inst_name), "locnames" => $this->splitNames($iterator->sp_location)];
135
            }
136
        }
137
        return $this->SPList;
138
    }
139
140
    public function countAllServiceProviders() {
141
        if ($this->counter > -1) {
142
            return $this->counter;
143
        }
144
145
        $cachedNumber = @file_get_contents(ROOT . "/var/tmp/cachedSPNumber.serialised");
146
        if ($cachedNumber !== FALSE) {
147
            $numberData = unserialize($cachedNumber);
148
            $now = new \DateTime();
149
            $cacheDate = $numberData["timestamp"]; // this is a DateTime object
150
            $diff = $now->diff($cacheDate);
151
            if ($diff->y == 0 && $diff->m == 0 && $diff->d == 0) {
152
                $this->counter = $numberData["number"];
153
                return $this->counter;
154
            }
155
        } else { // data in cache is too old or doesn't exist. We really need to ask the database
156
            $list = $this->listAllServiceProviders();
157
            $this->counter = count($list);
158
            file_put_contents(ROOT . "/var/tmp/cachedSPNumber.serialised", serialize(["number" => $this->counter, "timestamp" => new \DateTime()]));
159
            return $this->counter;
160
        }
161
    }
162
163
    public const TYPE_IDPSP = "3";
164
    public const TYPE_SP = "2";
165
    public const TYPE_IDP = "1";
166
    private const TYPE_MAPPING = [
167
        IdP::TYPE_IDP => ExternalEduroamDBData::TYPE_IDP,
168
        IdP::TYPE_IDPSP => ExternalEduroamDBData::TYPE_IDPSP,
169
        IdP::TYPE_SP => ExternalEduroamDBData::TYPE_SP,
170
    ];
171
    
172
    /**
173
     * separate institution names as written in the eduroam DB into array
174
     * 
175
     * @param string $collapsed - '#' separated list of names - each name has
176
     *      two-letter language prefic followed by ':'
177
     * @return array $nameList - tle list contains both separate per-lang entires
178
     *        and a joint one, just names no lang info - this used for comparison with CAT institution names
179
     */
180
    public static function dissectCollapsedInstitutionNames($collapsed) {
181
        $names = explode('#', $collapsed);
182
        $nameList = [
183
            'joint' => [],
184
            'perlang' => [],
185
        ];
186
        foreach ($names as $name) {
187
            $perlang = explode(': ', $name, 2);
188
            $nameList['perlang'][$perlang[0]] = $perlang[1];
189
            if (!in_array($perlang[1], $nameList['joint'])) {
190
                $nameList['joint'][] = mb_strtolower(preg_replace('/^..: /', '', $name), 'UTF-8');
191
            }
192
        } 
193
        return $nameList;
194
    }
195
196
    /**
197
     * separate institution realms as written in the eduroam DB into array
198
     * 
199
     * @param type $collapsed
0 ignored issues
show
Bug introduced by
The type core\type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
200
     * @return array $realmList
201
     */
202
    public static function dissectCollapsedInstitutionRealms($collapsed) {
203
        if ($collapsed === '' || $collapsed === NULL) {
204
            return [];
205
        }
206
        $realmList = explode(',', $collapsed);
207
        return $realmList;        
208
    }
209
    
210
    /**
211
     * 
212
     * @param string $collapsed the blob with contact info from eduroam DB
213
     * @return array of contacts represented as array name,mail,phone
214
     */
215
    public static function dissectCollapsedContacts($collapsed) {
216
        $contacts = explode('#', $collapsed);
217
        $contactList = [];
218
        foreach ($contacts as $contact) {
219
            $matches = [];
220
            preg_match("/^n: *([^ ].*), e: *([^ ].*), p: *([^ ].*)$/", $contact, $matches);
221
            if (!isset($matches[1])) {
222
                continue;
223
            }
224
            $contactList[] = [
225
                "name" => $matches[1],
226
                "mail" => $matches[2],
227
                "phone" => $matches[3]
228
            ];
229
        }
230
        return $contactList;
231
    }  
232
233
    /**
234
     * retrieves entity information from the eduroam database. Choose whether to get all entities with an SP role, an IdP role, or only those with both roles
235
     * 
236
     * @param string      $tld  the top-level domain from which to fetch the entities
237
     * @param string|NULL $type type of entity to retrieve
238
     * @return array list of entities
239
     */
240
    public function listExternalEntities($tld, $type) {
241
        if ($type === NULL) {
242
            $eduroamDbType = NULL;
243
        } else {
244
            $eduroamDbType = self::TYPE_MAPPING[$type]; // anything
245
        }
246
        $returnarray = [];
247
        $query = "SELECT instid AS id, country, inst_realm as realmlist, name AS collapsed_name, contact AS collapsed_contact, type FROM view_active_institution WHERE country = ?";
248
        if ($eduroamDbType !== NULL) {
249
            $query .= " AND ( type = '" . ExternalEduroamDBData::TYPE_IDPSP . "' OR type = '" . $eduroamDbType . "')";
250
        }
251
        $externals = $this->db->exec($query, "s", $tld);
252
        // was a SELECT query, so a resource and not a boolean
253
        while ($externalQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $externals)) {
254
            $names = $this->splitNames($externalQuery->collapsed_name);
255
            $thelanguage = $names[$this->languageInstance->getLang()] ?? $names["en"] ?? array_shift($names);
256
            $contacts = $this::dissectCollapsedContacts($externalQuery->collapsed_contact);
257
            $mails = [];
258
            foreach ($contacts as $contact) {
259
                // extracting real names is nice, but the <> notation
260
                // really gets screwed up on POSTs and HTML safety
261
                // so better not do this; use only mail addresses
262
                $mails[] = $contact['mail'];
263
            }
264
            $convertedType = array_search($externalQuery->type, self::TYPE_MAPPING);
265
            $returnarray[] = ["ID" => $externalQuery->id, "name" => $thelanguage, "contactlist" => implode(", ", $mails), "country" => $externalQuery->country, "realmlist" => $externalQuery->realmlist, "type" => $convertedType];
266
        }
267
        usort($returnarray, array($this, "usortInstitution"));
268
        return $returnarray;
269
    }
270
271
    /**
272
     * retrieves entity information from the eduroam database having the given realm in the inst_realm field
273
     * Choose which fields to get or get default
274
     * 
275
     * @param string      $realm  the realm
276
     * @param array       $fields list of fields
277
     * @return array list of entities
278
     */
279
    public function listExternalEntitiesByRealm($realm, $fields = []) {
280
        $returnArray = [];
281
        $defaultFields = ['instid', 'country', 'inst_realm', 'name', 'contact', 'type'];
282
        if (empty($fields)) {
283
            $fields = $defaultFields;
284
        }
285
        $forSelect = join(', ', $fields);
286
        //$query = "SELECT $forSelect FROM view_active_institution WHERE inst_realm like ?'";
287
        $query = "SELECT $forSelect FROM view_active_institution WHERE inst_realm like '%$realm%'";
288
        //$externals = $this->db->exec($query, "s", $realm);
289
        $externals = $this->db->exec($query);
290
        // was a SELECT query, so a resource and not a boolean
291
        while ($externalQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $externals)) {
292
            $record = [];
293
            foreach ($fields as $field) {
294
                $record[$field] = $externalQuery->$field;
295
            }
296
            $returnArray[] = $record;
297
        }
298
        return $returnArray;
299
    }
300
    
301
    /*
302
     * retrieves the list of identifiers (external and local) of all institutions
303
     * which have the admin email listed in the externam DB, thos that are synced to an
304
     * existing CAT institution will also have the local identifier (else NULL)
305
     */
306
    
307
    public function listExternalEntitiesByUserEmail($userEmail){
308
        $out = [];
309
        $cat = $this->localDb->dbName;
310
        $query = "SELECT DISTINCT view_institution_admins.instid, $cat.institution.inst_id,
311
            UPPER(view_active_institution.country), view_active_institution.name,
312
            view_active_institution.inst_realm, view_active_institution.type
313
            FROM view_institution_admins
314
            JOIN view_active_institution
315
                ON view_institution_admins.instid = view_active_institution.instid
316
            LEFT JOIN $cat.institution
317
                ON view_institution_admins.instid=$cat.institution.external_db_id
318
            WHERE view_active_institution.type != 2 AND view_institution_admins.email= ?";
319
        
320
        $externals = $this->db->exec($query, 's', $userEmail);
321
        while ($row = $externals->fetch_array()) {
322
            $external_db_id =  $row[0];
323
            $inst_id = $row[1];
324
            $country = $row[2];
325
            $name = $row[3];
326
            $realm = $row[4];
327
            $type = $row[5];
328
            if (!isset($out[$country])) {
329
                $out[$country] = [];
330
            }
331
            $out[$country][] = ['external_db_id'=>$external_db_id, 'inst_id'=>$inst_id, 'name'=>$name, 'realm'=>$realm, 'type'=>$type];
332
        }
333
        return $out;
334
    }
335
    
336
    /**
337
     * Test if a given external institution exists and if userEmail is provided also
338
     * check if this mail is listed as the admin for this institutution
339
     * 
340
     * @param type $ROid
341
     * @param type $extId
342
     * @param type $userEmail
343
     * @return int 1 if found 0 if not
344
     */
345
    public function verifyExternalEntity($ROid, $extId, $userEmail = NULL) {
346
        $query = "SELECT * FROM view_institution_admins WHERE ROid='$ROid' AND instid='$extId'";
347
        if ($userEmail != NULL) {
348
            $query .= " AND email='$userEmail'";
349
        }
350
        $result = $this->db->exec($query);
351
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $result) > 0) {
352
            return 1;
353
        } else {
354
            return 0;
355
        }
356
    }  
357
    
358
    public function listExternalRealms() {
359
        return $this->listExternalEntitiesByRealm(""); // leaing realm empty gets *all*
360
    }
361
362
    /**
363
     * helper function to sort institutions by their name
364
     * 
365
     * @param array $a an array with institution a's information
366
     * @param array $b an array with institution b's information
367
     * @return int the comparison result
368
     */
369
    private function usortInstitution($a, $b) {
370
        return strcasecmp($a["name"], $b["name"]);
371
    }
372
373
    /**
374
     * get all RADIUS/TLS servers for a given federation, with contacts
375
     * [ hostnames => contacts ]
376
     * (hostnames are comma-separated)
377
     * 
378
     * @return array
379
     */
380
    public function listExternalTlsServersFederation($tld) {
381
        $retval = [];
382
        // this includes servers of type "staging", which is fine
383
        $query = "SELECT servers, contacts FROM view_tls_ro WHERE country = ? AND servers IS NOT NULL AND contacts IS NOT NULL";
384
        $roTldServerTransaction = $this->db->exec($query, "s", $tld);
385
        while ($roServerResponses = mysqli_fetch_object(/** @scrutinizer ignore-type */ $roTldServerTransaction)) {
386
            // there is only one row_id
387
            $retval[$roServerResponses->servers] = $this::dissectCollapsedContacts($roServerResponses->contacts);
388
        }
389
        return $retval;
390
    }
391
392
    /**
393
     * get all RADIUS/TLS servers for all institutions within a given federation
394
     * including their contact details
395
     * 
396
     * "ROid-instid" => [type, inst_name, servers, contacts]
397
     * 
398
     * (hostnames are comma-separated)
399
     * 
400
     * @return array
401
     */
402
    public function listExternalTlsServersInstitution($tld, $include_not_ready=FALSE) {
403
        $retval = [];
404
        // this includes servers of type "staging", which is fine
405
        $query = "SELECT ROid, instid, type, inst_name, servers, contacts, ts FROM view_tls_inst WHERE country = ?";
406
        if (!$include_not_ready) {
407
            $query = $query . " AND servers IS NOT NULL AND contacts IS NOT NULL";
408
        }
409
        $instServerTransaction = $this->db->exec($query, "s", $tld);
410
        while ($instServerResponses = mysqli_fetch_object(/** @scrutinizer ignore-type */ $instServerTransaction)) {
411
            $contactList = $this::dissectCollapsedContacts($instServerResponses->contacts);
412
            $names = $this->splitNames($instServerResponses->inst_name);
413
            $thelanguage = $names[$this->languageInstance->getLang()] ?? $names["en"] ?? array_shift($names);
414
            $retval[$instServerResponses->ROid . "-". $instServerResponses->instid] = [
415
                "names" => $names,
416
                "name" => $thelanguage,
417
                "type" => array_search($instServerResponses->type, self::TYPE_MAPPING),
418
                "servers" => $instServerResponses->servers,
419
                "contacts" => $contactList,
420
                "ts" => $instServerResponses->ts];
421
        }
422
        uasort($retval, array($this, "usortInstitution"));
423
        return $retval;        
424
    }     
425
}
426