Passed
Push — master ( 92b079...56770a )
by Tomasz
15:05
created

EntityWithDBProperties::getAttributes()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
dl 0
loc 24
rs 8.4444
cc 8
nc 8
nop 2
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 Federation, IdP and Profile classes.
25
 * These should be split into separate files later.
26
 *
27
 * @package Developer
28
 */
29
/**
30
 * 
31
 */
32
33
namespace core;
34
35
use Exception;
36
37
/**
38
 * This class represents an Entity with properties stored in the DB.
39
 * IdPs have properties of their own, and may have one or more Profiles. The
40
 * profiles can override the institution-wide properties.
41
 *
42
 * @author Stefan Winter <[email protected]>
43
 * @author Tomasz Wolniewicz <[email protected]>
44
 *
45
 * @license see LICENSE file in root directory
46
 */
47
abstract class EntityWithDBProperties extends \core\common\Entity
48
{
49
50
    /**
51
     * This variable gets initialised with the known IdP attributes in the constructor. It never gets updated until the object
52
     * is destroyed. So if attributes change in the database, and IdP attributes are to be queried afterwards, the object
53
     * needs to be re-instantiated to have current values in this variable.
54
     * 
55
     * @var array of entity's attributes
56
     */
57
    protected $attributes;
58
59
    /**
60
     * The database to query for attributes regarding this entity
61
     * 
62
     * @var string DB type
63
     */
64
    protected $databaseType;
65
66
    /**
67
     * This variable contains the name of the table that stores the entity's options
68
     * 
69
     * @var string DB table name
70
     */
71
    protected $entityOptionTable;
72
73
    /**
74
     * column name to find entity in that table
75
     * 
76
     * @var string DB column name of entity
77
     */
78
    protected $entityIdColumn;
79
80
    /**
81
     * We need database access. Be sure to instantiate the singleton, and then
82
     * use its instance (rather than always accessing everything statically)
83
     * 
84
     * @var DBConnection the instance of the default database we talk to usually
85
     */
86
    protected $databaseHandle;
87
88
    /**
89
     * the unique identifier of this entity instance
90
     * refers to the integer row_id name in the DB -> int; Federation has no own
91
     * DB, so the identifier is of no use there -> use Fedearation->$tld
92
     * 
93
     * @var integer identifier of the entity instance
94
     */
95
    public $identifier;
96
97
    /**
98
     * the name of the entity in the current locale
99
     * 
100
     * @var string
101
     */
102
    public $name;
103
104
    /**
105
     * The constructor initialises the entity. Since it has DB properties,
106
     * this means the DB connection is set up for it.
107
     * 
108
     * @throws Exception
109
     */
110
    public function __construct()
111
    {
112
        parent::__construct();
113
        // we are called after the sub-classes have declared their default
114
        // database instance in $databaseType
115
        $handle = DBConnection::handle($this->databaseType);
116
        if ($handle instanceof DBConnection) {
117
            $this->databaseHandle = $handle;
118
        } else {
119
            throw new Exception("This database type is never an array!");
120
        }
121
        $this->attributes = [];
122
    }
123
124
    /**
125
     * How is the object identified in the database?
126
     * @return string|int
127
     * @throws Exception
128
     */
129
    private function getRelevantIdentifier()
130
    {
131
        switch (get_class($this)) {
132
            case "core\ProfileRADIUS":
133
            case "core\ProfileSilverbullet":
134
            case "core\IdP":
135
            case "core\DeploymentManaged":
136
                return $this->identifier;
137
            case "core\Federation":
138
                return $this->tld;
139
            case "core\User":
140
                return $this->userName;
141
            default:
142
                throw new Exception("Operating on a class where we don't know the relevant identifier in the DB - " . get_class($this) . "!");
143
        }
144
    }
145
146
    /**
147
     * This function retrieves the entity's attributes. 
148
     * 
149
     * If called with two optional parameters, only attribute values for the attribute
150
     * name in $optionName are retrieved; otherwise, all attributes are retrieved
151
     * unless $omittedOptionName is set - this attribute will be filtered out (the
152
     * main use is to filter out logos).
153
     * The retrieval is in-memory from the internal attributes class member - no
154
     * DB callback, so changes in the database during the class instance lifetime
155
     * are not considered.
156
     *
157
     * @param string $optionName optionally, the name of the attribute that is to be retrieved
158
     * @param string$omittedOptionName optionally drop attibutes with that name
0 ignored issues
show
Bug introduced by
The type core\optionally 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...
159
     * @return array of arrays of attributes which were set for this IdP
160
     */
161
    public function getAttributes(string $optionName = NULL, string $omittedOptionName = NULL)
162
    {
163
        if ($optionName !== NULL) {
164
            if ($optionName === $omittedOptionName) {
165
                throw new Exception("The attibute to be shown has the same name as that to be omitted");
166
            }
167
            $returnarray = [];
168
            foreach ($this->attributes as $theAttr) {
169
                if ($theAttr['name'] == $optionName) {
170
                    $returnarray[] = $theAttr;
171
                }
172
            }
173
            return $returnarray;
174
        }
175
        if ($omittedOptionName !== NULL) {
176
            $returnarray = [];
177
            foreach ($this->attributes as $theAttr) {
178
                if ($theAttr['name'] !== $omittedOptionName) {
179
                    $returnarray[] = $theAttr;
180
                }
181
            }
182
            return $returnarray;
183
        }
184
        return $this->attributes;
185
    }
186
187
188
    /**
189
     * deletes all attributes in this profile except the _file ones, these are reported as array
190
     *
191
     * @param string $extracondition a condition to append to the deletion query. RADIUS Profiles have eap-level or device-level options which shouldn't be purged; this can be steered in the overriding function.
192
     * @return array list of row_id id's of file-based attributes which weren't deleted
193
     */
194
    public function beginFlushAttributes($extracondition = "")
195
    {
196
        $quotedIdentifier = (!is_int($this->getRelevantIdentifier()) ? "\"" : "") . $this->getRelevantIdentifier() . (!is_int($this->getRelevantIdentifier()) ? "\"" : "");
197
        $this->databaseHandle->exec("DELETE FROM $this->entityOptionTable WHERE $this->entityIdColumn = $quotedIdentifier AND option_name NOT LIKE '%_file' $extracondition");
198
        $this->updateFreshness();
199
        $execFlush = $this->databaseHandle->exec("SELECT row_id FROM $this->entityOptionTable WHERE $this->entityIdColumn = $quotedIdentifier $extracondition");
200
        $returnArray = [];
201
        // SELECT always returns a resource, never a boolean
202
        while ($queryResult = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execFlush)) {
203
            $returnArray[$queryResult->row_id] = "KILLME";
204
        }
205
        return $returnArray;
206
    }
207
208
    /**
209
     * after a beginFlushAttributes, deletes all attributes which are in the tobedeleted array.
210
     *
211
     * @param array $tobedeleted array of database rows which are to be deleted
212
     * @return void
213
     */
214
    public function commitFlushAttributes(array $tobedeleted)
215
    {
216
        $quotedIdentifier = (!is_int($this->getRelevantIdentifier()) ? "\"" : "") . $this->getRelevantIdentifier() . (!is_int($this->getRelevantIdentifier()) ? "\"" : "");
217
        foreach (array_keys($tobedeleted) as $row_id) {
218
            $this->databaseHandle->exec("DELETE FROM $this->entityOptionTable WHERE $this->entityIdColumn = $quotedIdentifier AND row_id = $row_id");
219
            $this->updateFreshness();
220
        }
221
    }
222
223
    /**
224
     * deletes all attributes of this entity from the database
225
     * 
226
     * @return void
227
     */
228
    public function flushAttributes()
229
    {
230
        $this->commitFlushAttributes($this->beginFlushAttributes());
231
    }
232
233
    /**
234
     * Adds an attribute for the entity instance into the database. Multiple instances of the same attribute are supported.
235
     *
236
     * @param string $attrName  Name of the attribute. This must be a well-known value from the profile_option_dict table in the DB.
237
     * @param string $attrLang  language of the attribute. Can be NULL.
238
     * @param mixed  $attrValue Value of the attribute. Can be anything; will be stored in the DB as-is.
239
     * @return void
240
     */
241
    public function addAttribute($attrName, $attrLang, $attrValue)
242
    {
243
        $relevantId = $this->getRelevantIdentifier();
244
        $identifierType = (is_int($relevantId) ? "i" : "s");
245
        $this->databaseHandle->exec("INSERT INTO $this->entityOptionTable ($this->entityIdColumn, option_name, option_lang, option_value) VALUES(?,?,?,?)", $identifierType . "sss", $relevantId, $attrName, $attrLang, $attrValue);
246
        $this->updateFreshness();
247
    }
248
249
    /**
250
     * retrieve attributes from a database. Only does SELECT queries.
251
     * @param string $query sub-classes set the query to execute to get to the options
252
     * @param string $level the retrieved options get flagged with this "level" identifier
253
     * @return array the attributes in one array
254
     * @throws Exception
255
     */
256
    protected function retrieveOptionsFromDatabase($query, $level)
257
    {
258
        if (substr($query, 0, 6) != "SELECT") {
259
            throw new Exception("This function only operates with SELECT queries!");
260
        }
261
        $optioninstance = Options::instance();
262
        $tempAttributes = [];
263
        $relevantId = $this->getRelevantIdentifier();
264
        $attributeDbExec = $this->databaseHandle->exec($query, is_int($relevantId) ? "i" : "s", $relevantId);
265
        if (empty($attributeDbExec)) {
266
            return $tempAttributes;
267
        }
268
        // with SELECTs, we always operate on a resource, not a boolean
269
        while ($attributeQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $attributeDbExec)) {
270
            $optinfo = $optioninstance->optionType($attributeQuery->option_name);
271
            $flag = $optinfo['flag'];
272
            $decoded = $attributeQuery->option_value;
273
            // file attributes always get base64-decoded.
274
            if ($optinfo['type'] == 'file') {
275
                $decoded = base64_decode($decoded);
276
            }
277
            $tempAttributes[] = ["name" => $attributeQuery->option_name, "lang" => $attributeQuery->option_lang, "value" => $decoded, "level" => $level, "row_id" => $attributeQuery->row_id, "flag" => $flag];
278
        }
279
        return $tempAttributes;
280
    }
281
282
    /**
283
     * Retrieves data from the underlying tables, for situations where instantiating the IdP or Profile object is inappropriate
284
     * 
285
     * @param string $table institution_option or profile_option
286
     * @param int    $row_id   rowindex
287
     * @return string|boolean the data, or FALSE if something went wrong
288
     */
289
    public static function fetchRawDataByIndex($table, $row_id)
290
    {
291
        // only for select tables!
292
        switch ($table) {
293
            case "institution_option":
294
            // fall-through intended
295
            case "profile_option":
296
            // fall-through intended
297
            case "federation_option":
298
                break;
299
            default:
300
                return FALSE;
301
        }
302
        $handle = DBConnection::handle("INST");
303
        $blobQuery = $handle->exec("SELECT option_value from $table WHERE row_id = $row_id");
304
        // SELECT -> returns resource, not boolean
305
        $dataset = mysqli_fetch_row(/** @scrutinizer ignore-type */ $blobQuery);
306
        return $dataset[0] ?? FALSE;
307
    }
308
309
    /**
310
     * Checks if a raw data pointer is public data (return value FALSE) or if 
311
     * yes who the authorised admins to view it are (return array of user IDs)
312
     * 
313
     * @param string $table which database table is this about
314
     * @param int    $row_id   row_id index of the table
315
     * @return mixed FALSE if the data is public, an array of owners of the data if it is NOT public
316
     */
317
    public static function isDataRestricted($table, $row_id)
318
    {
319
        if ($table != "institution_option" && $table != "profile_option" && $table != "federation_option" && $table != "user_options") {
320
            return []; // better safe than sorry: that's an error, so assume nobody is authorised to act on that data
321
        }
322
        // we need to create our own DB handle as this is a static method
323
        $handle = DBConnection::handle("INST");
324
        switch ($table) {
325
            case "profile_option": // both of these are similar
326
                $columnName = "profile_id";
327
            // fall-through intended
328
            case "institution_option":
329
                $blobId = -1;
330
                $columnName = $columnName ?? "institution_id";
331
                $blobQuery = $handle->exec("SELECT $columnName as id from $table WHERE row_id = ?", "i", $row_id);
332
                // SELECT always returns a resource, never a boolean
333
                while ($idQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $blobQuery)) { // only one row_id
334
                    $blobId = $idQuery->id;
335
                }
336
                if ($blobId == -1) {
337
                    return []; // err on the side of caution: we did not find any data. It's a severe error, but not fatal. Nobody owns non-existent data.
338
                }
339
340
                if ($table == "profile_option") { // is the profile in question public?
341
                    $profile = ProfileFactory::instantiate($blobId);
342
                    if ($profile->readinessLevel() == AbstractProfile::READINESS_LEVEL_SHOWTIME) { // public data
343
                        return FALSE;
344
                    }
345
                    // okay, so it's NOT public. prepare to return the owner
346
                    $inst = new IdP($profile->institution);
347
                } else { // does the IdP have at least one public profile?
348
                    $inst = new IdP($blobId);
349
                    // if at least one of the profiles belonging to the inst is public, the data is public
350
                    if ($inst->maxProfileStatus() == IdP::PROFILES_SHOWTIME) { // public data
351
                        return FALSE;
352
                    }
353
                }
354
                // okay, so it's NOT public. return the owner
355
                return $inst->listOwners();
356
            case "federation_option":
357
                // federation metadata is always public
358
                return FALSE;
359
            // user options are never public
360
            case "user_options":
361
                return [];
362
            default:
363
                return []; // better safe than sorry: that's an error, so assume nobody is authorised to act on that data
364
        }
365
    }
366
367
    /**
368
     * join new attributes to existing ones, but only if not already defined on
369
     * a different level in the existing set
370
     * 
371
     * @param array  $existing the already existing attributes
372
     * @param array  $new      the new set of attributes
373
     * @param string $newlevel the level of the new attributes
374
     * @return array the new set of attributes
375
     */
376
    protected function levelPrecedenceAttributeJoin($existing, $new, $newlevel)
377
    {
378
        foreach ($new as $attrib) {
379
            $ignore = "";
380
            foreach ($existing as $approvedAttrib) {
381
                if (($attrib["name"] == $approvedAttrib["name"] && $approvedAttrib["level"] != $newlevel) && ($approvedAttrib["name"] != "device-specific:redirect")) {
382
                    $ignore = "YES";
383
                }
384
            }
385
            if ($ignore != "YES") {
386
                $existing[] = $attrib;
387
            }
388
        }
389
        return $existing;
390
    }
391
392
    /**
393
     * when options in the DB change, this can mean generated installers become stale. sub-classes must define whether this is the case for them
394
     * 
395
     * @return void
396
     */
397
    abstract public function updateFreshness();
398
}
399