Passed
Push — master ( 4302eb...519f93 )
by Stefan
11:45
created

levelPrecedenceAttributeJoin()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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