Test Failed
Push — master ( 0071b0...4282d1 )
by Stefan
05:33
created

EntityWithDBProperties::addAttribute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
/*
3
 * *****************************************************************************
4
 * Contributions to this work were made on behalf of the GÉANT project, a 
5
 * project that has received funding from the European Union’s Framework 
6
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
7
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
8
 * 691567 (GN4-1) and No. 731122 (GN4-2).
9
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
10
 * of the copyright in all material which was developed by a member of the GÉANT
11
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
12
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
13
 * UK as a branch of GÉANT Vereniging.
14
 * 
15
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
16
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
17
 *
18
 * License: see the web/copyright.inc.php file in the file structure or
19
 *          <base_url>/copyright.php after deploying the software
20
 */
21
22
/**
23
 * This file contains Federation, IdP and Profile classes.
24
 * These should be split into separate files later.
25
 *
26
 * @package Developer
27
 */
28
/**
29
 * 
30
 */
31
32
namespace core;
33
use Exception;
34
35
/**
36
 * This class represents an Entity with properties stored in the DB.
37
 * IdPs have properties of their own, and may have one or more Profiles. The
38
 * profiles can override the institution-wide properties.
39
 *
40
 * @author Stefan Winter <[email protected]>
41
 * @author Tomasz Wolniewicz <[email protected]>
42
 *
43
 * @license see LICENSE file in root directory
44
 */
45
abstract class EntityWithDBProperties extends \core\common\Entity {
46
47
    /**
48
     * This variable gets initialised with the known IdP attributes in the constructor. It never gets updated until the object
49
     * is destroyed. So if attributes change in the database, and IdP attributes are to be queried afterwards, the object
50
     * needs to be re-instantiated to have current values in this variable.
51
     * 
52
     * @var array of entity's attributes
53
     */
54
    protected $attributes;
55
56
    /**
57
     * The database to query for attributes regarding this entity
58
     * 
59
     * @var string DB type
60
     */
61
    protected $databaseType;
62
63
    /**
64
     * This variable contains the name of the table that stores the entity's options
65
     * 
66
     * @var string DB table name
67
     */
68
    protected $entityOptionTable;
69
70
    /**
71
     * column name to find entity in that table
72
     * 
73
     * @var string DB column name of entity
74
     */
75
    protected $entityIdColumn;
76
77
    /**
78
     * We need database access. Be sure to instantiate the singleton, and then
79
     * use its instance (rather than always accessing everything statically)
80
     * 
81
     * @var DBConnection the instance of the default database we talk to usually
82
     */
83
    protected $databaseHandle;
84
85
    /**
86
     * the unique identifier of this entity instance
87
     * refers to the integer row name in the DB -> int; Federation has no own
88
     * DB, so the identifier is of no use there -> use Fedearation->$tld
89
     * 
90
     * @var int identifier of the entity instance
91
     */
92
    public $identifier;
93
94
    /**
95
     * the name of the entity in the current locale
96
     */
97
    public $name;
98
99
    /**
100
     * The constructor initialises the entity. Since it has DB properties,
101
     * this means the DB connection is set up for it.
102
     */
103
    public function __construct() {
104
        parent::__construct();
105
        // we are called after the sub-classes have declared their default
106
        // databse instance in $databaseType
107
        $this->databaseHandle = DBConnection::handle($this->databaseType);
108
        $this->attributes = [];
109
    }
110
111
    /**
112
     * How is the object identified in the database?
113
     * @return string|int
114
     * @throws Exception
115
     */
116
    private function getRelevantIdentifier() {
117
        switch (get_class($this)) {
118
            case "core\ProfileRADIUS":
119
            case "core\ProfileSilverbullet":
120
            case "core\IdP":
121
            case "core\DeploymentManaged":
122
                return $this->identifier;
123
            case "core\Federation":
124
                return $this->tld;
125
            case "core\User":
126
                return $this->userName;
127
            default:
128
                throw new Exception("Operating on a class where we don't know the relevant identifier in the DB - ".get_class($this)."!");
129
        }
130
    }
131
    
132
    /**
133
     * This function retrieves the entity's attributes. 
134
     * 
135
     * If called with the optional parameter, only attribute values for the attribute
136
     * name in $optionName are retrieved; otherwise, all attributes are retrieved.
137
     * The retrieval is in-memory from the internal attributes class member - no
138
     * DB callback, so changes in the database during the class instance lifetime
139
     * are not considered.
140
     *
141
     * @param string $optionName optionally, the name of the attribute that is to be retrieved
142
     * @return array of arrays of attributes which were set for this IdP
143
     */
144
    public function getAttributes(string $optionName = NULL) {
145
        if ($optionName !== NULL) {
146
            $returnarray = [];
147
            foreach ($this->attributes as $theAttr) {
148
                if ($theAttr['name'] == $optionName) {
149
                    $returnarray[] = $theAttr;
150
                }
151
            }
152
            return $returnarray;
153
        }
154
        return $this->attributes;
155
    }
156
    
157
    /**
158
     * deletes all attributes in this profile except the _file ones, these are reported as array
159
     *
160
     * @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.
161
     * @return array list of row id's of file-based attributes which weren't deleted
162
     */
163
    public function beginFlushAttributes($extracondition = "") {
164
        $quotedIdentifier = (!is_int($this->getRelevantIdentifier()) ? "\"" : "") . $this->getRelevantIdentifier() . (!is_int($this->getRelevantIdentifier()) ? "\"" : "");
165
        $this->databaseHandle->exec("DELETE FROM $this->entityOptionTable WHERE $this->entityIdColumn = $quotedIdentifier AND option_name NOT LIKE '%_file' $extracondition");
166
        $this->updateFreshness();
167
        $execFlush = $this->databaseHandle->exec("SELECT row FROM $this->entityOptionTable WHERE $this->entityIdColumn = $quotedIdentifier $extracondition");
168
        $returnArray = [];
169
        // SELECT always returns a resourse, never a boolean
170
        while ($queryResult = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execFlush)) {
171
            $returnArray[$queryResult->row] = "KILLME";
172
        }
173
        return $returnArray;
174
    }
175
176
    /**
177
     * after a beginFlushAttributes, deletes all attributes which are in the tobedeleted array.
178
     *
179
     * @param array $tobedeleted array of database rows which are to be deleted
180
     * @return void
181
     */
182
    public function commitFlushAttributes(array $tobedeleted) {
183
        $quotedIdentifier = (!is_int($this->getRelevantIdentifier()) ? "\"" : "") . $this->getRelevantIdentifier() . (!is_int($this->getRelevantIdentifier()) ? "\"" : "");
184
        foreach (array_keys($tobedeleted) as $row) {
185
            $this->databaseHandle->exec("DELETE FROM $this->entityOptionTable WHERE $this->entityIdColumn = $quotedIdentifier AND row = $row");
186
            $this->updateFreshness();
187
        }
188
    }
189
190
    /**
191
     * deletes all attributes of this entity from the database
192
     * 
193
     * @return void
194
     */
195
    public function flushAttributes() {
196
        $this->commitFlushAttributes($this->beginFlushAttributes());
197
    }
198
199
    /**
200
     * Adds an attribute for the entity instance into the database. Multiple instances of the same attribute are supported.
201
     *
202
     * @param string $attrName  Name of the attribute. This must be a well-known value from the profile_option_dict table in the DB.
203
     * @param string $attrLang  language of the attribute. Can be NULL.
204
     * @param mixed  $attrValue Value of the attribute. Can be anything; will be stored in the DB as-is.
205
     * @return void
206
     */
207
    public function addAttribute($attrName, $attrLang, $attrValue) {
208
        $relevantId = $this->getRelevantIdentifier();
209
        $identifierType = (is_int($relevantId) ? "i" : "s");
210
        $this->databaseHandle->exec("INSERT INTO $this->entityOptionTable ($this->entityIdColumn, option_name, option_lang, option_value) VALUES(?,?,?,?)", $identifierType . "sss", $relevantId, $attrName, $attrLang, $attrValue);
211
        $this->updateFreshness();
212
    }
213
214
    /**
215
     * retrieve attributes from a database. Only does SELECT queries.
216
     * @param string $query sub-classes set the query to execute to get to the options
217
     * @param string $level the retrieved options get flagged with this "level" identifier
218
     * @return array the attributes in one array
219
     */
220
    protected function retrieveOptionsFromDatabase($query, $level) {
221
        if (substr($query, 0, 6) != "SELECT") {
222
            throw new Exception("This function only operates with SELECT queries!");
223
        }
224
        $optioninstance = Options::instance();
225
        $tempAttributes = [];
226
        $relevantId = $this->getRelevantIdentifier();
227
        $attributeDbExec = $this->databaseHandle->exec($query, is_int($relevantId) ? "i" : "s", $relevantId);
228
        if (empty($attributeDbExec)) {
229
            return $tempAttributes;
230
        }
231
        // with SELECTs, we always operate on a resource, not a boolean
232
        while ($attributeQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $attributeDbExec)) {
233
            $optinfo = $optioninstance->optionType($attributeQuery->option_name);
234
            $flag = $optinfo['flag'];
235
            $decoded = $attributeQuery->option_value;
236
            // file attributes always get base64-decoded.
237
            if ($optinfo['type'] == 'file') {
238
                $decoded = base64_decode($decoded);
239
            }
240
            $tempAttributes[] = ["name" => $attributeQuery->option_name, "lang" => $attributeQuery->option_lang, "value" => $decoded, "level" => $level, "row" => $attributeQuery->row, "flag" => $flag];
241
        }
242
        return $tempAttributes;
243
    }
244
245
    /**
246
     * Retrieves data from the underlying tables, for situations where instantiating the IdP or Profile object is inappropriate
247
     * 
248
     * @param string $table institution_option or profile_option
249
     * @param string $row   rowindex
250
     * @return string|boolean the data, or FALSE if something went wrong
251
     */
252
    public static function fetchRawDataByIndex($table, $row) {
253
        // only for select tables!
254
        if ($table != "institution_option" && $table != "profile_option" && $table != "federation_option") {
255
            return FALSE;
256
        }
257
        if (!is_numeric($row)) {
258
            return FALSE;
259
        }
260
261
        $handle = DBConnection::handle("INST");
262
        $blobQuery = $handle->exec("SELECT option_value from $table WHERE row = $row");
263
        // SELECT -> returns resource, not boolean
264
        while ($returnedData =  mysqli_fetch_object(/** @scrutinizer ignore-type */ $blobQuery)) {
265
            $blob = $returnedData->option_value;
266
        }
267
        if (!isset($blob)) {
268
            return FALSE;
269
        }
270
        return $blob;
271
    }
272
273
    /**
274
     * Checks if a raw data pointer is public data (return value FALSE) or if 
275
     * yes who the authorised admins to view it are (return array of user IDs)
276
     * 
277
     * @param string $table which database table is this about
278
     * @param int    $row   row index of the table
279
     * @return mixed FALSE if the data is public, an array of owners of the data if it is NOT public
280
     */
281
    public static function isDataRestricted($table, $row) {
282
        if ($table != "institution_option" && $table != "profile_option" && $table != "federation_option" && $table != "user_options") {
283
            return []; // better safe than sorry: that's an error, so assume nobody is authorised to act on that data
284
        }
285
        // we need to create our own DB handle as this is a static method
286
        $handle = DBConnection::handle("INST");
287
        switch ($table) {
288
            case "profile_option": // both of these are similar
289
                $columnName = "profile_id";
290
                // fall-through intended
291
            case "institution_option":
292
                $blobId = -1;
293
                $columnName = $columnName ?? "institution_id";
294
                $blobQuery = $handle->exec("SELECT $columnName as id from $table WHERE row = ?", "i", $row);
295
                // SELECT always returns a resourse, never a boolean
296
                while ($idQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $blobQuery)) { // only one row
297
                    $blobId = $idQuery->id;
298
                }
299
                if ($blobId == -1) {
300
                    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.
301
                }
302
                
303
                if ($table == "profile_option") { // is the profile in question public?
304
                    $profile = ProfileFactory::instantiate($blobId);
305
                    if ($profile->readinessLevel() == AbstractProfile::READINESS_LEVEL_SHOWTIME) { // public data
306
                        return FALSE;
307
                    }
308
                    // okay, so it's NOT public. prepare to return the owner
309
                    $inst = new IdP($profile->institution);
310
                } else { // does the IdP have at least one public profile?
311
                    $inst = new IdP($blobId);
312
                    // if at least one of the profiles belonging to the inst is public, the data is public
313
                    if ($inst->maxProfileStatus() == IdP::PROFILES_SHOWTIME) { // public data
314
                        return FALSE;
315
                    }
316
                }
317
                // okay, so it's NOT public. return the owner
318
                return $inst->listOwners();
319
            case "federation_option":
320
                // federation metadata is always public
321
                return FALSE;
322
            // user options are never public
323
            case "user_options":
324
                return [];
325
            default:
326
                return []; // better safe than sorry: that's an error, so assume nobody is authorised to act on that data
327
        }
328
    }
329
330
    /**
331
     * when options in the DB change, this can mean generated installers become stale. sub-classes must define whether this is the case for them
332
     * 
333
     * @return void
334
     */
335
    abstract public function updateFreshness();
336
}
337