Passed
Push — master ( dc5c7a...a6c5e6 )
by Stefan
06:48
created

Federation   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 62
eloc 208
dl 0
loc 423
rs 3.44
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A updateFreshness() 0 4 2
A downloadStatsCore() 0 23 3
A __construct() 0 48 4
B downloadStats() 0 33 8
A findCandidates() 0 16 5
A determineIdPIdByRealm() 0 21 4
A listFederationAdmins() 0 15 5
B listIdentityProviders() 0 38 8
B newIdP() 0 68 10
C listExternalEntities() 0 49 13

How to fix   Complexity   

Complex Class

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