Passed
Push — master ( 09e385...6d1e0a )
by Stefan
04:22
created

EntityWithDBProperties::getAttributeValue()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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