Completed
Push — master ( af3ce8...b4d558 )
by Stefan
12:25
created

IdP::getAllProfileStatusOverview()   C

Complexity

Conditions 7
Paths 33

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 15
c 2
b 1
f 0
nc 33
nop 0
dl 0
loc 22
rs 6.9811
1
<?php
2
3
/* * ********************************************************************************
4
 * (c) 2011-15 GÉANT on behalf of the GN3, GN3plus and GN4 consortia
5
 * License: see the LICENSE file in the root directory
6
 * ********************************************************************************* */
7
?>
8
<?php
9
10
/**
11
 * This file contains Federation, IdP and Profile classes.
12
 * These should be split into separate files later.
13
 *
14
 * @package Developer
15
 */
16
/**
17
 * 
18
 */
19
require_once('Helper.php');
20
require_once('Profile.php');
21
require_once("CAT.php");
22
require_once("Options.php");
23
require_once("DBConnection.php");
24
require_once("RADIUSTests.php");
25
require_once('EntityWithDBProperties.php');
26
27
define("EXTERNAL_DB_SYNCSTATE_NOT_SYNCED", 0);
28
define("EXTERNAL_DB_SYNCSTATE_SYNCED", 1);
29
define("EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING", 2);
30
31
/**
32
 * This class represents an Identity Provider (IdP).
33
 * IdPs have properties of their own, and may have one or more Profiles. The
34
 * profiles can override the institution-wide properties.
35
 *
36
 * @author Stefan Winter <[email protected]>
37
 * @author Tomasz Wolniewicz <[email protected]>
38
 *
39
 * @license see LICENSE file in root directory
40
 *
41
 * @package Developer
42
 */
43
class IdP extends EntityWithDBProperties {
44
45
    /**
46
     *
47
     * @var int synchronisation state with external database, if any
48
     */
49
    private $externalDbSyncstate;
50
51
    /**
52
     * The shortname of this IdP's federation
53
     * @var string 
54
     */
55
    public $federation;
56
57
    /**
58
     * Constructs an IdP object based on its details in the database.
59
     * Cannot be used to define a new IdP in the database! This happens via Federation::newIdP()
60
     *
61
     * @param integer $instId the database row identifier
62
     */
63
    public function __construct($instId) {
64
        debug(3, "--- BEGIN Constructing new IdP object ... ---\n");
65
66
        $this->databaseType = "INST";
67
        $this->entityOptionTable = "institution_option";
68
        $this->entityIdColumn = "inst_id";
69
        $this->identifier = $instId;
70
        $this->attributes = [];
71
72
        $idp = DBConnection::exec($this->databaseType, "SELECT inst_id, country,external_db_syncstate FROM institution WHERE inst_id = $this->identifier");
73
        if (!$attributeQuery = mysqli_fetch_object($idp)) {
74
            throw new Exception("IdP $this->identifier not found in database!");
75
        }
76
77
        $this->federation = $attributeQuery->country;
78
79
        $optioninstance = Options::instance();
80
81
        $this->externalDbSyncstate = $attributeQuery->externalDbSyncstate;
82
        // fetch attributes from DB and keep them in priv_attributes
83
84
        $idPAttributes = DBConnection::exec($this->databaseType, "SELECT DISTINCT option_name,option_value, row FROM institution_option
85
              WHERE institution_id = $this->identifier  ORDER BY option_name");
86
87 View Code Duplication
        while ($attributeQuery = mysqli_fetch_object($idPAttributes)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
88
            // decode base64 for files (respecting multi-lang)
89
            $optinfo = $optioninstance->optionType($attributeQuery->option_name);
90
            $flag = $optinfo['flag'];
91
92
            if ($optinfo['type'] != "file") {
93
                $this->attributes[] = ["name" => $attributeQuery->option_name, "value" => $attributeQuery->option_value, "level" => "IdP", "row" => $attributeQuery->row, "flag" => $flag];
94
            } else {
95
                $decodedAttribute = $this->decodeFileAttribute($attributeQuery->option_value);
96
97
                $this->attributes[] = ["name" => $attributeQuery->option_name, "value" => ($decodedAttribute['lang'] == "" ? $decodedAttribute['content'] : serialize($decodedAttribute)), "level" => "IdP", "row" => $attributeQuery->row, "flag" => $flag];
98
            }
99
        }
100
        $this->attributes[] = ["name" => "internal:country",
101
            "value" => $this->federation,
102
            "level" => "IdP",
103
            "row" => 0,
104
            "flag" => NULL];
105
106
        $this->name = getLocalisedValue($this->getAttributes('general:instname'), CAT::get_lang());
107
        debug(3, "--- END Constructing new IdP object ... ---\n");
108
    }
109
110
    /**
111
     * This function retrieves all registered profiles for this IdP from the database
112
     *
113
     * @return array List of Profiles of this IdP
114
     * @param int $activeOnly if and set to non-zero will
115
     * cause listing of only those institutions which have some valid profiles defined.
116
     */
117
    public function listProfiles($activeOnly = 0) {
118
        $query = "SELECT profile_id FROM profile WHERE inst_id = $this->identifier" . ($activeOnly ? " AND showtime = 1" : "");
119
        $allProfiles = DBConnection::exec($this->databaseType, $query);
120
        $returnarray = [];
121
        while ($profileQuery = mysqli_fetch_object($allProfiles)) {
122
            $oneProfile = new Profile($profileQuery->profile_id, $this);
0 ignored issues
show
Documentation introduced by
$this is of type this<IdP>, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
123
            $oneProfile->institution = $this->identifier;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->identifier can also be of type string. However, the property $institution is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
124
            $returnarray[] = $oneProfile;
125
        }
126
        return $returnarray;
127
    }
128
129 View Code Duplication
    public function isOneProfileConfigured() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
130
        $allProfiles = DBConnection::exec($this->databaseType, "SELECT profile_id FROM profile WHERE inst_id = $this->identifier AND sufficient_config = 1");
131
        if (mysqli_num_rows($allProfiles) > 0) {
132
            return TRUE;
133
        }
134
        return FALSE;
135
    }
136
137 View Code Duplication
    public function isOneProfileShowtime() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
138
        $allProfiles = DBConnection::exec($this->databaseType, "SELECT profile_id FROM profile WHERE inst_id = $this->identifier AND showtime = 1");
139
        if (mysqli_num_rows($allProfiles) > 0) {
140
            return TRUE;
141
        }
142
        return FALSE;
143
    }
144
145
    public function getAllProfileStatusOverview() {
146
        $allProfiles = DBConnection::exec($this->databaseType, "SELECT status_dns, status_cert, status_reachability, status_TLS, last_status_check FROM profile WHERE inst_id = $this->identifier AND sufficient_config = 1");
147
        $returnarray = ['dns' => RETVAL_SKIPPED, 'cert' => L_OK, 'reachability' => RETVAL_SKIPPED, 'TLS' => RETVAL_SKIPPED, 'checktime' => NULL];
148
        while ($statusQuery = mysqli_fetch_object($allProfiles)) {
149
            if ($statusQuery->status_dns < $returnarray['dns']) {
150
                $returnarray['dns'] = $statusQuery->status_dns;
151
            }
152
            if ($statusQuery->status_reachability < $returnarray['reachability']) {
153
                $returnarray['reachability'] = $statusQuery->status_reachability;
154
            }
155
            if ($statusQuery->status_TLS < $returnarray['TLS']) {
156
                $returnarray['TLS'] = $statusQuery->status_TLS;
157
            }
158
            if ($statusQuery->status_cert < $returnarray['cert']) {
159
                $returnarray['cert'] = $statusQuery->status_cert;
160
            }
161
            if ($statusQuery->last_status_check > $returnarray['checktime']) {
162
                $returnarray['checktime'] = $statusQuery->last_status_check;
163
            }
164
        }
165
        return $returnarray;
166
    }
167
168
    /** This function retrieves an array of authorised users which can
169
     * manipulate this institution.
170
     * 
171
     * @return array owners of the institution; numbered array with members ID, MAIL and LEVEL
172
     */
173
    public function owner() {
174
        $returnarray = [];
175
        $admins = DBConnection::exec($this->databaseType, "SELECT user_id, orig_mail, blesslevel FROM ownership WHERE institution_id = $this->identifier ORDER BY user_id");
176
        while ($ownerQuery = mysqli_fetch_object($admins)) {
177
            $returnarray[] = ['ID' => $ownerQuery->user_id, 'MAIL' => $ownerQuery->orig_mail, 'LEVEL' => $ownerQuery->blesslevel];
178
        }
179
        return $returnarray;
180
    }
181
182
    /**
183
     * This function gets the profile count for a given IdP
184
     * The count could be retreived from the listProfiles method
185
     * but this is less expensive.
186
     *
187
     * @return int profile count
188
     */
189
    public function profileCount() {
190
        $result = DBConnection::exec($this->databaseType, "SELECT profile_id FROM profile 
191
             WHERE inst_id = $this->identifier");
192
        return(mysqli_num_rows($result));
193
    }
194
195
    /**
196
     * This function sets the timestamp of last modification of the child profiles to the current timestamp. This is needed
197
     * for installer caching: all installers which are on disk must be re-created if an attribute changes. This timestamp here
198
     * is used to determine if the installer on disk is still new enough.
199
     */
200
    public function updateFreshness() {
201
        // freshness is always defined for *Profiles*
202
        // IdP needs to update timestamp of all its profiles if an IdP-wide attribute changed
203
        DBConnection::exec($this->databaseType, "UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE inst_id = '$this->identifier'");
204
    }
205
206
    /**
207
     * Adds a new profile to this IdP.
208
     * Only creates the DB entry for the Profile. If you want to add attributes later, see Profile::addAttribute().
209
     *
210
     * @return object new Profile object if successful, or FALSE if an error occured
211
     */
212
    public function newProfile() {
213
        DBConnection::exec($this->databaseType, "INSERT INTO profile (inst_id) VALUES($this->identifier)");
214
        $identifier = DBConnection::lastID($this->databaseType);
215
216
        if ($identifier > 0) {
217
            return new Profile($identifier, $this);
0 ignored issues
show
Documentation introduced by
$this is of type this<IdP>, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
218
        }
219
        return NULL;
220
    }
221
222
    /**
223
     * deletes the IdP and all its profiles
224
     */
225
    public function destroy() {
226
        /* delete all profiles */
227
        foreach ($this->listProfiles() as $profile) {
228
            $profile->destroy();
229
        }
230
        /* double-check that all profiles are gone */
231
        $profiles = $this->listProfiles();
232
233
        if (count($profiles) > 0) {
234
            die("This IdP shouldn't have any profiles any more!");
235
        }
236
237
        DBConnection::exec($this->databaseType, "DELETE FROM ownership WHERE institution_id = $this->identifier");
238
        DBConnection::exec($this->databaseType, "DELETE FROM institution_option WHERE institution_id = $this->identifier");
239
        DBConnection::exec($this->databaseType, "DELETE FROM institution WHERE inst_id = $this->identifier");
240
241
        // notify federation admins
242
243
        $fed = new Federation($this->federation);
244
        foreach ($fed->listFederationAdmins() as $id) {
245
            $user = new User($id);
246
            $message = sprintf(_("Hi,
247
248
the Identity Provider %s in your %s federation %s has been deleted from %s.
249
250
We thought you might want to know.
251
252
Best regards,
253
254
%s"), $this->name, Config::$CONSORTIUM['name'], strtoupper($fed->name), Config::$APPEARANCE['productname'], Config::$APPEARANCE['productname_long']);
255
            $user->sendMailToUser(_("IdP in your federation was deleted"), $message);
256
        }
257
        unset($this);
258
    }
259
260
    /**
261
     * Performs a lookup in an external database to determine matching entities to this IdP. The business logic of this function is
262
     * roaming consortium specific; if no match algorithm is known for the consortium, FALSE is returned.
263
     * 
264
     * @return array list of entities in external database that correspond to this IdP or FALSE if no consortium-specific matching function is defined
265
     */
266
    public function getExternalDBSyncCandidates() {
267
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
268
            $list = [];
269
            $usedarray = [];
270
            // extract all institutions from the country
271
            $candidateList = DBConnection::exec("EXTERNAL", "SELECT id_institution AS id, name AS collapsed_name FROM view_active_idp_institution WHERE country = '" . strtolower($this->federation) . "'");
272
273
            $alreadyUsed = DBConnection::exec($this->databaseType, "SELECT DISTINCT external_db_id FROM institution WHERE external_db_id IS NOT NULL AND external_db_syncstate = " . EXTERNAL_DB_SYNCSTATE_SYNCED);
274
            while ($alreadyUsedQuery = mysqli_fetch_object($alreadyUsed)) {
275
                $usedarray[] = $alreadyUsedQuery->external_db_id;
276
            }
277
278
            // and split them into ID, LANG, NAME pairs
279
            while ($candidateListQuery = mysqli_fetch_object($candidateList)) {
280
                if (in_array($candidateListQuery->id, $usedarray)) {
281
                    continue;
282
                }
283
                $names = explode('#', $candidateListQuery->collapsed_name);
284
                foreach ($names as $name) {
285
                    $perlang = explode(': ', $name, 2);
286
                    $list[] = ["ID" => $candidateListQuery->id, "lang" => $perlang[0], "name" => $perlang[1]];
287
                }
288
            }
289
            // now see if any of the languages in CAT match any of those in the external DB
290
            $mynames = $this->getAttributes("general:instname");
291
            $matchingCandidates = [];
292
            foreach ($mynames as $onename) {
293
                foreach ($list as $listentry) {
294
                    $unserialised = unserialize($onename['value']);
295
                    if (($unserialised['lang'] == $listentry['lang'] || $unserialised['lang'] == "C") && $unserialised['content'] == $listentry['name']) {
296
                        if (array_search($listentry['ID'], $matchingCandidates) === FALSE) {
297
                            $matchingCandidates[] = $listentry['ID'];
298
                        }
299
                    }
300
                }
301
            }
302
            return $matchingCandidates;
303
        }
304
        return FALSE;
305
    }
306
307
    public function getExternalDBSyncState() {
308
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
309
            return $this->externalDbSyncstate;
310
        }
311
        return EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING;
312
    }
313
314
    /**
315
     * Retrieves the external DB identifier of this institution. Returns FALSE if no ID is known.
316
     * 
317
     * @return int the external identifier; or FALSE if no external ID is known
318
     */
319
    public function getExternalDBId() {
320
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
321
            $id = DBConnection::exec($this->databaseType, "SELECT external_db_id FROM institution WHERE inst_id = $this->identifier AND external_db_syncstate = " . EXTERNAL_DB_SYNCSTATE_SYNCED);
322
            if (mysqli_num_rows($id) == 0) {
323
                return FALSE;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return FALSE; (false) is incompatible with the return type documented by IdP::getExternalDBId of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
324
            } else {
325
                $externalIdQuery = mysqli_fetch_object($id);
326
                return $externalIdQuery->external_db_id;
327
            }
328
        }
329
        return FALSE;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return FALSE; (false) is incompatible with the return type documented by IdP::getExternalDBId of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
330
    }
331
332
    /**
333
     * Fetches information from the external database about this IdP
334
     * 
335
     * @return array details about that institution. Array may be empty if entity is not synced
336
     */
337
    public function getExternalDBEntityDetails() {
338
        $externalId = $this->getExternalDBId();
339
        if ($externalId !== FALSE) {
340
            return Federation::getExternalDBEntityDetails($externalId);
341
        }
342
        return [];
343
    }
344
345
    public function setExternalDBId($identifier) {
346
        $escapedIdentifier = DBConnection::escape_value($this->databaseType, $identifier);
347
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
348
            $alreadyUsed = DBConnection::exec($this->databaseType, "SELECT DISTINCT external_db_id FROM institution WHERE external_db_id = '$escapedIdentifier' AND external_db_syncstate = " . EXTERNAL_DB_SYNCSTATE_SYNCED);
349
350
            if (mysqli_num_rows($alreadyUsed) == 0) {
351
                DBConnection::exec($this->databaseType, "UPDATE institution SET external_db_id = '$escapedIdentifier', external_db_syncstate = " . EXTERNAL_DB_SYNCSTATE_SYNCED . " WHERE inst_id = $this->identifier");
352
            }
353
        }
354
    }
355
}
356