Completed
Push — master ( 03ec64...ee79b5 )
by
unknown
07:25
created

IdP::isAttributeAvailable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
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 $external_db_syncstate;
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 $i_id the database row identifier
62
     */
63
    public function __construct($i_id) {
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 = $i_id;
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 (!$a = mysqli_fetch_object($idp)) {
74
            throw new Exception("IdP $this->identifier not found in database!");
75
        }
76
        
77
        $this->federation = $a->country;
78
79
        $optioninstance = Options::instance();
80
        
81
        $this->external_db_syncstate = $a->external_db_syncstate;
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 ($a = 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
            $lang = "";
89
            // decode base64 for files (respecting multi-lang)
90
            $optinfo = $optioninstance->optionType($a->option_name);
91
            $flag = $optinfo['flag'];
92
93
            if ($optinfo['type'] != "file") {
94
                $this->attributes[] = ["name" => $a->option_name, "value" => $a->option_value, "level" => "IdP", "row" => $a->row, "flag" => $flag];
95
            } else {
96
                // suppress E_NOTICE on the following... we are testing *if*
97
                // we have a serialized value - so not having one is fine and
98
                // shouldn't throw E_NOTICE
99
                if (@unserialize($a->option_value) !== FALSE) { // multi-lang
100
                    $content = unserialize($a->option_value);
101
                    $lang = $content['lang'];
102
                    $content = $content['content'];
103
                } else { // single lang, direct content
104
                    $content = $a->option_value;
105
                }
106
107
                $content = base64_decode($content);
108
109
                $this->attributes[] = ["name" => $a->option_name, "value" => ($lang == "" ? $content : serialize(['lang' => $lang, 'content' => $content])), "level" => "IdP", "row" => $a->row, "flag" => $flag];
110
            }
111
        }
112
        $this->attributes[] = ["name" => "internal:country", 
113
                                         "value" => $this->federation, 
114
                                         "level" => "IdP", 
115
                                         "row" => 0, 
116
                                         "flag" => NULL];
117
118
        $this->name = getLocalisedValue($this->getAttributes('general:instname'), CAT::get_lang());
119
        debug(3, "--- END Constructing new IdP object ... ---\n");
120
    }
121
122
    /**
123
     * This function retrieves all registered profiles for this IdP from the database
124
     *
125
     * @return array List of Profiles of this IdP
126
     * @param int $active_only if and set to non-zero will
127
     * cause listing of only those institutions which have some valid profiles defined.
128
     */
129
    public function listProfiles($active_only = 0) {
130
        $query = "SELECT profile_id FROM profile WHERE inst_id = $this->identifier";
131
        if ($active_only)
132
            $query .= " AND showtime = 1";
133
        $allProfiles = DBConnection::exec($this->databaseType, $query);
134
        $returnarray = [];
135
        while ($a = mysqli_fetch_object($allProfiles)) {
136
            $k = new Profile($a->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...
137
            $k->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...
138
            $returnarray[] = $k;
139
        }
140
        return $returnarray;
141
    }
142
    
143
    public function isOneProfileConfigured() {
144
        // migration phase: are there NULLs in the profile list sufficient_config column?
145
        // if so, run prepShowtime on all profiles
146
        $needTreatment = DBConnection::exec($this->databaseType, "SELECT profile_id FROM profile WHERE inst_id = $this->identifier AND sufficient_config IS NULL");
147
        if (mysqli_num_rows($needTreatment) > 0)
148
            foreach ($this->listProfiles() as $prof)
149
                $prof->prepShowtime();
150
       
151
        // now, just look in the DB
152
        $allProfiles = DBConnection::exec($this->databaseType, "SELECT profile_id FROM profile WHERE inst_id = $this->identifier AND sufficient_config = 1");
153
        if (mysqli_num_rows($allProfiles) > 0)
154
            return TRUE;
155
        else
156
            return FALSE;
157
    }
158
159
    public function isOneProfileShowtime() {
160
        $allProfiles = DBConnection::exec($this->databaseType, "SELECT profile_id FROM profile WHERE inst_id = $this->identifier AND showtime = 1");
161
        if (mysqli_num_rows($allProfiles) > 0)
162
            return TRUE;
163
        else
164
            return FALSE;
165
        
166
    }
167
    
168
    public function getAllProfileStatusOverview() {
169
        $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");
170
        $returnarray = ['dns' => RETVAL_SKIPPED, 'cert' => L_OK, 'reachability' => RETVAL_SKIPPED, 'TLS' => RETVAL_SKIPPED, 'checktime' => NULL];
171
        while ($a = mysqli_fetch_object($allProfiles)) {
172
            if ($a->status_dns < $returnarray['dns'])
173
                $returnarray['dns'] = $a->status_dns;
174
            if ($a->status_reachability < $returnarray['reachability'])
175
                $returnarray['reachability'] = $a->status_reachability;
176
            if ($a->status_TLS < $returnarray['TLS'])
177
                $returnarray['TLS'] = $a->status_TLS;
178
            if ($a->status_cert < $returnarray['cert'])
179
                $returnarray['cert'] = $a->status_cert;
180
            if ($a->last_status_check > $returnarray['checktime'])
181
                $returnarray['checktime'] = $a->last_status_check;
182
        }
183
        return $returnarray;
184
    }
185
    
186
    /** This function retrieves an array of authorised users which can
187
     * manipulate this institution.
188
     * 
189
     * @return array owners of the institution; numbered array with members ID, MAIL and LEVEL
190
     */
191
    public function owner() {
192
        $returnarray = [];
193
        $admins = DBConnection::exec($this->databaseType, "SELECT user_id, orig_mail, blesslevel FROM ownership WHERE institution_id = $this->identifier ORDER BY user_id");
194
        while ($a = mysqli_fetch_object($admins))
195
            $returnarray[] = ['ID' => $a->user_id, 'MAIL' => $a->orig_mail, 'LEVEL' => $a->blesslevel];
196
        return $returnarray;
197
    }
198
199
    /**
200
     * This function gets the profile count for a given IdP
201
     * The count could be retreived from the listProfiles method
202
     * but this is less expensive.
203
     *
204
     * @return int profile count
205
     */
206
    public function profileCount() {
207
        $result = DBConnection::exec($this->databaseType, "SELECT profile_id FROM profile 
208
             WHERE inst_id = $this->identifier");
209
        return(mysqli_num_rows($result));
210
    }
211
212
    /**
213
     * This function sets the timestamp of last modification of the child profiles to the current timestamp. This is needed
214
     * for installer caching: all installers which are on disk must be re-created if an attribute changes. This timestamp here
215
     * is used to determine if the installer on disk is still new enough.
216
     */
217
    public function updateFreshness() {
218
        // freshness is always defined for *Profiles*
219
        // IdP needs to update timestamp of all its profiles if an IdP-wide attribute changed
220
        DBConnection::exec($this->databaseType, "UPDATE profile SET last_change = CURRENT_TIMESTAMP WHERE inst_id = '$this->identifier'");
221
    }
222
223
    /**
224
     * Adds a new profile to this IdP.
225
     * Only creates the DB entry for the Profile. If you want to add attributes later, see Profile::addAttribute().
226
     *
227
     * @return object new Profile object if successful, or FALSE if an error occured
228
     */
229
    public function newProfile() {
230
        DBConnection::exec($this->databaseType, "INSERT INTO profile (inst_id) VALUES($this->identifier)");
231
        $identifier = DBConnection::lastID($this->databaseType);
232
233
        if ($identifier > 0)
234
            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...
235
        else
236
            return NULL;
237
    }
238
239
    /**
240
     * deletes the IdP and all its profiles
241
     */
242
    public function destroy() {
243
        /* delete all profiles */
244
        foreach ($this->listProfiles() as $profile)
245
            $profile->destroy();
246
        /* double-check that all profiles are gone */
247
        $profiles = $this->listProfiles();
248
249
        if (count($profiles) > 0)
250
            die("This IdP shouldn't have any profiles any more!");
251
252
        DBConnection::exec($this->databaseType, "DELETE FROM ownership WHERE institution_id = $this->identifier");
253
        DBConnection::exec($this->databaseType, "DELETE FROM institution_option WHERE institution_id = $this->identifier");
254
        DBConnection::exec($this->databaseType, "DELETE FROM institution WHERE inst_id = $this->identifier");
255
256
        // notify federation admins
257
258
        $fed = new Federation($this->federation);
259
        foreach ($fed->listFederationAdmins() as $id) {
260
            $user = new User($id);
261
            $message = sprintf(_("Hi,
262
263
the Identity Provider %s in your %s federation %s has been deleted from %s.
264
265
We thought you might want to know.
266
267
Best regards,
268
269
%s"), $this->name, Config::$CONSORTIUM['name'], strtoupper($fed->name), Config::$APPEARANCE['productname'], Config::$APPEARANCE['productname_long']);
270
            $user->sendMailToUser(_("IdP in your federation was deleted"), $message);
271
        }
272
        unset($this);
273
    }
274
275
    /**
276
     * Performs a lookup in an external database to determine matching entities to this IdP. The business logic of this function is
277
     * roaming consortium specific; if no match algorithm is known for the consortium, FALSE is returned.
278
     * 
279
     * @return array list of entities in external database that correspond to this IdP or FALSE if no consortium-specific matching function is defined
280
     */
281
    public function getExternalDBSyncCandidates() {
282
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
283
            $list = [];
284
            $usedarray = [];
285
            // extract all institutions from the country
286
            $candidate_list = DBConnection::exec("EXTERNAL", "SELECT id_institution AS id, name AS collapsed_name FROM view_active_idp_institution WHERE country = '" . strtolower($this->federation) . "'");
287
288
            $already_used = 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);
289
            while ($a = mysqli_fetch_object($already_used))
290
                $usedarray[] = $a->external_db_id;
291
292
            // and split them into ID, LANG, NAME pairs
293
            while ($a = mysqli_fetch_object($candidate_list)) {
294
                if (in_array($a->id, $usedarray))
295
                    continue;
296
                $names = explode('#', $a->collapsed_name);
297
                foreach ($names as $name) {
298
                    $perlang = explode(': ', $name, 2);
299
                    $list[] = ["ID" => $a->id, "lang" => $perlang[0], "name" => $perlang[1]];
300
                }
301
            }
302
            // now see if any of the languages in CAT match any of those in the external DB
303
            $mynames = $this->getAttributes("general:instname");
304
            $matching_candidates = [];
305
            foreach ($mynames as $onename)
306
                foreach ($list as $listentry) {
307
                    $unserialised = unserialize($onename['value']);
308
                    if (($unserialised['lang'] == $listentry['lang'] || $unserialised['lang'] == "C") && $unserialised['content'] == $listentry['name'])
309
                        if (array_search($listentry['ID'], $matching_candidates) === FALSE)
310
                            $matching_candidates[] = $listentry['ID'];
311
                }
312
313
            return $matching_candidates;
314
        }
315
        return FALSE;
316
    }
317
318
    public function getExternalDBSyncState() {
319
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
320
            return $this->external_db_syncstate;
321
        }
322
        return EXTERNAL_DB_SYNCSTATE_NOTSUBJECTTOSYNCING;
323
    }
324
325
    /**
326
     * Retrieves the external DB identifier of this institution. Returns FALSE if no ID is known.
327
     * 
328
     * @return int the external identifier; or FALSE if no external ID is known
329
     */
330
    public function getExternalDBId() {
331
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
332
            $id = DBConnection::exec($this->databaseType, "SELECT external_db_id FROM institution WHERE inst_id = $this->identifier AND external_db_syncstate = " . EXTERNAL_DB_SYNCSTATE_SYNCED);
333
            if (mysqli_num_rows($id) == 0) {
334
                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...
335
            } else {
336
                $a = mysqli_fetch_object($id);
337
                return $a->external_db_id;
338
            }
339
        }
340
        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...
341
    }
342
343
    /**
344
     * Fetches information from the external database about this IdP
345
     * 
346
     * @return array details about that institution. Array may be empty if entity is not synced
347
     */
348
    public function getExternalDBEntityDetails() {
349
        $external_id = $this->getExternalDBId();
350
        if ($external_id !== FALSE)
351
            return Federation::getExternalDBEntityDetails($external_id);
352
        else
353
            return [];
354
    }
355
356
    public function setExternalDBId($identifier) {
357
        $identifier = DBConnection::escape_value($this->databaseType, $identifier);
358
        if (Config::$CONSORTIUM['name'] == "eduroam" && isset(Config::$CONSORTIUM['deployment-voodoo']) && Config::$CONSORTIUM['deployment-voodoo'] == "Operations Team") { // SW: APPROVED
359
            $already_used = DBConnection::exec($this->databaseType, "SELECT DISTINCT external_db_id FROM institution WHERE external_db_id = '$identifier' AND external_db_syncstate = " . EXTERNAL_DB_SYNCSTATE_SYNCED);
360
361
            if (mysqli_num_rows($already_used) == 0)
362
                DBConnection::exec($this->databaseType, "UPDATE institution SET external_db_id = '$identifier', external_db_syncstate = " . EXTERNAL_DB_SYNCSTATE_SYNCED . " WHERE inst_id = $this->identifier");
363
        }
364
    }
365
}