|
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 the UserManagement class. |
|
25
|
|
|
* |
|
26
|
|
|
* @author Stefan Winter <[email protected]> |
|
27
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
|
28
|
|
|
* |
|
29
|
|
|
* @license see LICENSE file in root directory |
|
30
|
|
|
* |
|
31
|
|
|
* @package Developer |
|
32
|
|
|
*/ |
|
33
|
|
|
/** |
|
34
|
|
|
* necessary includes |
|
35
|
|
|
*/ |
|
36
|
|
|
|
|
37
|
|
|
namespace core; |
|
38
|
|
|
|
|
39
|
|
|
use \Exception; |
|
|
|
|
|
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* This class manages user privileges and bindings to institutions |
|
43
|
|
|
* |
|
44
|
|
|
* @author Stefan Winter <[email protected]> |
|
45
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
|
46
|
|
|
* |
|
47
|
|
|
* @package Developer |
|
48
|
|
|
*/ |
|
49
|
|
|
class UserManagement extends \core\common\Entity |
|
50
|
|
|
{ |
|
51
|
|
|
|
|
52
|
|
|
/** |
|
53
|
|
|
* our handle to the INST database |
|
54
|
|
|
* |
|
55
|
|
|
* @var DBConnection |
|
56
|
|
|
*/ |
|
57
|
|
|
private $databaseHandle; |
|
58
|
|
|
public $currentInstitutions; |
|
59
|
|
|
public $newUser = false; |
|
60
|
|
|
public $hasPotenialNewInst = false; |
|
61
|
|
|
|
|
62
|
|
|
/** |
|
63
|
|
|
* Class constructor. Nothing special to be done when constructing. |
|
64
|
|
|
* |
|
65
|
|
|
* @throws Exception |
|
66
|
|
|
*/ |
|
67
|
|
|
public function __construct() |
|
68
|
|
|
{ |
|
69
|
|
|
parent::__construct(); |
|
70
|
|
|
$handle = DBConnection::handle(self::$databaseType); |
|
71
|
|
|
if ($handle instanceof DBConnection) { |
|
72
|
|
|
$this->databaseHandle = $handle; |
|
73
|
|
|
} else { |
|
74
|
|
|
throw new Exception("This database type is never an array!"); |
|
75
|
|
|
} |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* database which this class queries by default |
|
80
|
|
|
* |
|
81
|
|
|
* @var string |
|
82
|
|
|
*/ |
|
83
|
|
|
private static $databaseType = "INST"; |
|
84
|
|
|
|
|
85
|
|
|
const TOKENSTATUS_OK_NEW = 1; |
|
86
|
|
|
const TOKENSTATUS_OK_EXISTING = 2; |
|
87
|
|
|
const TOKENSTATUS_FAIL_ALREADYCONSUMED = -1; |
|
88
|
|
|
const TOKENSTATUS_FAIL_EXPIRED = -2; |
|
89
|
|
|
const TOKENSTATUS_FAIL_NONEXISTING = -3; |
|
90
|
|
|
|
|
91
|
|
|
/** |
|
92
|
|
|
* Checks if a given invitation token exists and is valid in the invitations database |
|
93
|
|
|
* returns a string with the following values: |
|
94
|
|
|
* |
|
95
|
|
|
* OK-NEW valid token exists, and is not attached to an existing institution. When consuming the token, a new inst will be created |
|
96
|
|
|
* OK-EXISTING valid token exists, and is attached to an existing institution. When consuming the token, user will be added as an admin |
|
97
|
|
|
* FAIL-NONEXISTINGTOKEN this token does not exist at all in the database |
|
98
|
|
|
* FAIL-ALREADYCONSUMED the token exists, but has been used before |
|
99
|
|
|
* FAIL-EXPIRED the token exists, but has expired |
|
100
|
|
|
* |
|
101
|
|
|
* @param string $token the invitation token |
|
102
|
|
|
* @return int |
|
103
|
|
|
*/ |
|
104
|
|
|
public function checkTokenValidity($token) |
|
105
|
|
|
{ |
|
106
|
|
|
$check = $this->databaseHandle->exec("SELECT invite_token, cat_institution_id |
|
107
|
|
|
FROM invitations |
|
108
|
|
|
WHERE invite_token = ? AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0", "s", $token); |
|
109
|
|
|
// SELECT -> resource, not boolean |
|
110
|
|
|
if ($tokenCheck = mysqli_fetch_object(/** @scrutinizer ignore-type */ $check)) { |
|
111
|
|
|
if ($tokenCheck->cat_institution_id === NULL) { |
|
112
|
|
|
return self::TOKENSTATUS_OK_NEW; |
|
113
|
|
|
} |
|
114
|
|
|
return self::TOKENSTATUS_OK_EXISTING; |
|
115
|
|
|
} |
|
116
|
|
|
// if we haven't returned from the function yet, it is an invalid token... |
|
117
|
|
|
// be a little verbose what's wrong with it |
|
118
|
|
|
$checkReason = $this->databaseHandle->exec("SELECT invite_token, used FROM invitations WHERE invite_token = ?", "s", $token); |
|
119
|
|
|
// SELECT -> resource, not boolean |
|
120
|
|
|
if ($invalidTokenCheck = mysqli_fetch_object(/** @scrutinizer ignore-type */ $checkReason)) { |
|
121
|
|
|
if ($invalidTokenCheck->used == 1) { |
|
122
|
|
|
return self::TOKENSTATUS_FAIL_ALREADYCONSUMED; |
|
123
|
|
|
} |
|
124
|
|
|
return self::TOKENSTATUS_FAIL_EXPIRED; |
|
125
|
|
|
} |
|
126
|
|
|
return self::TOKENSTATUS_FAIL_NONEXISTING; |
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
|
|
/** |
|
130
|
|
|
* This function creates a new IdP in the database based on a valid invitation token - or adds a new administrator |
|
131
|
|
|
* to an existing one. The institution is created for the logged-in user (second argument) who presents the token (first |
|
132
|
|
|
* argument). The tokens are created via createToken(). |
|
133
|
|
|
* |
|
134
|
|
|
* @param string $token The invitation token (must exist in the database and be valid). |
|
135
|
|
|
* @param string $owner Persistent User ID who becomes the administrator of the institution |
|
136
|
|
|
* @return IdP |
|
137
|
|
|
*/ |
|
138
|
|
|
public function createIdPFromToken(string $token, string $owner) |
|
139
|
|
|
{ |
|
140
|
|
|
new CAT(); // be sure that Entity's static members are initialised |
|
141
|
|
|
common\Entity::intoThePotatoes(); |
|
142
|
|
|
// the token either has cat_institution_id set -> new admin for existing inst |
|
143
|
|
|
// or contains a number of parameters from external DB -> set up new inst |
|
144
|
|
|
$instinfo = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, external_db_uniquehandle, invite_fortype |
|
145
|
|
|
FROM invitations |
|
146
|
|
|
WHERE invite_token = ? AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0", "s", $token); |
|
147
|
|
|
// SELECT -> resource, no boolean |
|
148
|
|
|
if ($invitationDetails = mysqli_fetch_assoc(/** @scrutinizer ignore-type */ $instinfo)) { |
|
149
|
|
|
if ($invitationDetails['cat_institution_id'] !== NULL) { // add new admin to existing IdP |
|
150
|
|
|
// we can't rely on a unique key on this table (user IDs |
|
151
|
|
|
// possibly too long), so run a query to find there's an |
|
152
|
|
|
// tuple already; and act accordingly |
|
153
|
|
|
$catId = $invitationDetails['cat_institution_id']; |
|
154
|
|
|
$level = $invitationDetails['invite_issuer_level']; |
|
155
|
|
|
$destMail = $invitationDetails['invite_dest_mail']; |
|
156
|
|
|
$existing = $this->databaseHandle->exec("SELECT user_id FROM ownership WHERE user_id = ? AND institution_id = ?", "si", $owner, $catId); |
|
157
|
|
|
// SELECT -> resource, not boolean |
|
158
|
|
|
if (mysqli_num_rows(/** @scrutinizer ignore-type */ $existing) > 0) { |
|
159
|
|
|
$this->databaseHandle->exec("UPDATE ownership SET blesslevel = ?, orig_mail = ? WHERE user_id = ? AND institution_id = ?", "sssi", $level, $destMail, $owner, $catId); |
|
160
|
|
|
} else { |
|
161
|
|
|
$this->databaseHandle->exec("INSERT INTO ownership (user_id, institution_id, blesslevel, orig_mail) VALUES(?, ?, ?, ?)", "siss", $owner, $catId, $level, $destMail); |
|
162
|
|
|
} |
|
163
|
|
|
common\Logging::writeAudit_s((string) $owner, "OWN", "IdP " . $invitationDetails['cat_institution_id'] . " - added user as owner"); |
|
164
|
|
|
common\Entity::outOfThePotatoes(); |
|
165
|
|
|
return new IdP($invitationDetails['cat_institution_id']); |
|
166
|
|
|
} |
|
167
|
|
|
// create new IdP |
|
168
|
|
|
$fed = new Federation($invitationDetails['country']); |
|
169
|
|
|
// find the best name for the entity: C if specified, otherwise English, otherwise whatever |
|
170
|
|
|
if ($invitationDetails['external_db_uniquehandle'] != NULL) { |
|
171
|
|
|
$idp = $this->newIdPFromExternal($invitationDetails['external_db_uniquehandle'], $fed, $invitationDetails, $owner); |
|
172
|
|
|
} else { |
|
173
|
|
|
$bestnameguess = $invitationDetails['name']; |
|
174
|
|
|
$idp = new IdP($fed->newIdP('TOKEN', $invitationDetails['invite_fortype'], $owner, $invitationDetails['invite_issuer_level'], $invitationDetails['invite_dest_mail'], $bestnameguess)); |
|
175
|
|
|
$idp->addAttribute("general:instname", 'C', $bestnameguess); |
|
176
|
|
|
} |
|
177
|
|
|
common\Logging::writeAudit_s($owner, "NEW", "IdP " . $idp->identifier . " - created from invitation"); |
|
178
|
|
|
|
|
179
|
|
|
// in case we have more admins in the queue which were invited to |
|
180
|
|
|
// administer the same inst but haven't redeemed their invitations |
|
181
|
|
|
// yet, then we will have to rewrite the invitations to point to the |
|
182
|
|
|
// newly created actual IdP rather than the placeholder entry in the |
|
183
|
|
|
// invitations table |
|
184
|
|
|
// which other pending invites do we have? |
|
185
|
|
|
|
|
186
|
|
|
$otherPending = $this->databaseHandle->exec("SELECT id |
|
187
|
|
|
FROM invitations |
|
188
|
|
|
WHERE invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0 AND name = ? AND country = ? AND ( cat_institution_id IS NULL OR external_db_uniquehandle IS NULL ) ", "ss", $invitationDetails['name'], $invitationDetails['country']); |
|
189
|
|
|
// SELECT -> resource, no boolean |
|
190
|
|
|
while ($pendingDetail = mysqli_fetch_object(/** @scrutinizer ignore-type */ $otherPending)) { |
|
191
|
|
|
$this->databaseHandle->exec("UPDATE invitations SET cat_institution_id = " . $idp->identifier . " WHERE id = " . $pendingDetail->id); |
|
192
|
|
|
} |
|
193
|
|
|
common\Entity::outOfThePotatoes(); |
|
194
|
|
|
return $idp; |
|
195
|
|
|
} |
|
196
|
|
|
} |
|
197
|
|
|
|
|
198
|
|
|
/** |
|
199
|
|
|
* create new institution based on the edxternalDB data |
|
200
|
|
|
* @param string $extId - the eduroam database identifier |
|
201
|
|
|
* @param object $fed - the CAT federation object where the institution should be created |
|
202
|
|
|
* @param string $owner |
|
203
|
|
|
* @return type |
|
|
|
|
|
|
204
|
|
|
*/ |
|
205
|
|
|
public function createIdPFromExternal($extId, $fed, $owner) |
|
206
|
|
|
{ |
|
207
|
|
|
$cat = new CAT(); |
|
208
|
|
|
$ROid = strtoupper($fed->tld).'01'; |
|
209
|
|
|
$externalinfo = $cat->getExternalDBEntityDetails($extId, $ROid); |
|
210
|
|
|
$invitationDetails = [ |
|
211
|
|
|
'invite_fortype' => $externalinfo['type'], |
|
212
|
|
|
'invite_issuer_level' => "FED", |
|
213
|
|
|
'invite_dest_mail' => $_SESSION['auth_email'], |
|
214
|
|
|
]; |
|
215
|
|
|
$idp = $this->newIdPFromExternal($extId, $fed, $invitationDetails, $owner, $externalinfo); |
|
216
|
|
|
common\Logging::writeAudit_s($owner, "NEW", "IdP " . $idp->identifier . " - created from auto-registration of $extId"); |
|
217
|
|
|
return $idp; |
|
|
|
|
|
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
/* |
|
221
|
|
|
* This is the common part of the code for createIdPFromToken and createIdPFromExternal |
|
222
|
|
|
*/ |
|
223
|
|
|
private function newIdPFromExternal($extId, $fed, $invitationDetails, $owner, $externalinfo = []) |
|
224
|
|
|
{ |
|
225
|
|
|
// see if we had a C language, and if not, pick a good candidate |
|
226
|
|
|
if ($externalinfo == []) { |
|
227
|
|
|
$cat = new CAT(); |
|
228
|
|
|
$ROid = strtoupper($fed->tld).'01'; |
|
229
|
|
|
$externalinfo = $cat->getExternalDBEntityDetails($extId, $ROid); |
|
230
|
|
|
} |
|
231
|
|
|
$bestnameguess = $externalinfo['names']['C'] ?? $externalinfo['names']['en'] ?? reset($externalinfo['names']); |
|
232
|
|
|
$idp = new IdP($fed->newIdP('SELF', $invitationDetails['invite_fortype'], $owner, $invitationDetails['invite_issuer_level'], $invitationDetails['invite_dest_mail'], $bestnameguess)); |
|
233
|
|
|
foreach ($externalinfo['names'] as $instlang => $instname) { |
|
234
|
|
|
$idp->addAttribute("general:instname", $instlang, $instname); |
|
235
|
|
|
} |
|
236
|
|
|
$idp->setExternalDBId($extId, strtolower($fed->tld)); |
|
237
|
|
|
$idp->addAttribute("general:instname", 'C', $bestnameguess); |
|
238
|
|
|
return $idp; |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
/** |
|
242
|
|
|
* Adds a new administrator to an existing IdP |
|
243
|
|
|
* @param IdP $idp institution to which the admin is to be added. |
|
244
|
|
|
* @param string $user persistent user ID that is to be added as an admin. |
|
245
|
|
|
* @return boolean This function always returns TRUE. |
|
246
|
|
|
*/ |
|
247
|
|
|
public function addAdminToIdp($idp, $user) |
|
248
|
|
|
{ |
|
249
|
|
|
$existing = $this->databaseHandle->exec("SELECT user_id FROM ownership WHERE user_id = ? AND institution_id = ?", "si", $user, $idp->identifier); |
|
250
|
|
|
// SELECT -> resource, not boolean |
|
251
|
|
|
if (mysqli_num_rows(/** @scrutinizer ignore-type */ $existing) == 0) { |
|
252
|
|
|
$this->databaseHandle->exec("INSERT INTO ownership (institution_id,user_id,blesslevel,orig_mail) VALUES(?, ?, 'FED', 'SELF-APPOINTED')", "is", $idp->identifier, $user); |
|
253
|
|
|
} |
|
254
|
|
|
return TRUE; |
|
255
|
|
|
} |
|
256
|
|
|
|
|
257
|
|
|
/** |
|
258
|
|
|
* Deletes an administrator from the IdP. If the IdP and user combination doesn't match, nothing happens. |
|
259
|
|
|
* @param IdP $idp institution from which the admin is to be deleted. |
|
260
|
|
|
* @param string $user persistent user ID that is to be deleted as an admin. |
|
261
|
|
|
* @return boolean This function always returns TRUE. |
|
262
|
|
|
*/ |
|
263
|
|
|
public function removeAdminFromIdP($idp, $user) |
|
264
|
|
|
{ |
|
265
|
|
|
$this->databaseHandle->exec("DELETE from ownership WHERE institution_id = $idp->identifier AND user_id = ?", "s", $user); |
|
266
|
|
|
return TRUE; |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
/** |
|
270
|
|
|
* Invalidates a token so that it can't be used any more. Tokens automatically expire after 24h, but can be invalidated |
|
271
|
|
|
* earlier, e.g. after having been used to create an institution. If the token doesn't exist in the DB or is already invalidated, |
|
272
|
|
|
* nothing happens. |
|
273
|
|
|
* |
|
274
|
|
|
* @param string $token the token to invalidate |
|
275
|
|
|
* @return boolean This function always returns TRUE. |
|
276
|
|
|
*/ |
|
277
|
|
|
public function invalidateToken($token) |
|
278
|
|
|
{ |
|
279
|
|
|
$this->databaseHandle->exec("UPDATE invitations SET used = 1 WHERE invite_token = ?", "s", $token); |
|
280
|
|
|
return TRUE; |
|
281
|
|
|
} |
|
282
|
|
|
|
|
283
|
|
|
/** |
|
284
|
|
|
* Creates a new invitation token. The token's main purpose is to be sent out by mail. The function either can generate a token for a new |
|
285
|
|
|
* administrator of an existing institution, or for a new institution. In the latter case, the institution only actually gets |
|
286
|
|
|
* created in the DB if the token is actually consumed via createIdPFromToken(). |
|
287
|
|
|
* |
|
288
|
|
|
* @param boolean $isByFedadmin is the invitation token created from a federation admin (TRUE) or from an existing inst admin (FALSE) |
|
289
|
|
|
* @param array $for identifiers (typically email addresses) for which the invitation is created |
|
290
|
|
|
* @param mixed $instIdentifier either an instance of the IdP class (for existing institutions to invite new admins) or a string (new institution - this is the inst name then) |
|
291
|
|
|
* @param string $externalId if the IdP to be created is related to an external DB entity, this parameter contains that ID |
|
292
|
|
|
* @param string $country if the institution is new (i.e. $inst is a string) this parameter needs to specify the federation of the new inst |
|
293
|
|
|
* @param string $partType the type of participant |
|
294
|
|
|
* @return mixed The function returns either the token (as string) or FALSE if something went wrong |
|
295
|
|
|
* @throws Exception |
|
296
|
|
|
*/ |
|
297
|
|
|
public function createTokens($isByFedadmin, $for, $instIdentifier, $externalId = 0, $country = 0, $partType = 0) |
|
298
|
|
|
{ |
|
299
|
|
|
$level = ($isByFedadmin ? "FED" : "INST"); |
|
300
|
|
|
$tokenList = []; |
|
301
|
|
|
foreach ($for as $oneDest) { |
|
302
|
|
|
$token = bin2hex(random_bytes(40)); |
|
303
|
|
|
if ($instIdentifier instanceof IdP) { |
|
304
|
|
|
$this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,cat_institution_id) VALUES(?, ?, ?, ?, ?)", "ssssi", $instIdentifier->type, $level, $oneDest, $token, $instIdentifier->identifier); |
|
305
|
|
|
$tokenList[$token] = $oneDest; |
|
306
|
|
|
} else if (func_num_args() == 5) { |
|
307
|
|
|
$ROid = strtoupper($country).'01'; |
|
308
|
|
|
$cat = new CAT(); |
|
309
|
|
|
$extinfo = $cat->getExternalDBEntityDetails($externalId, $ROid); |
|
310
|
|
|
$extCountry = $extinfo['country']; |
|
311
|
|
|
$extType = $extinfo['type']; |
|
312
|
|
|
if(\config\Master::FUNCTIONALITY_FLAGS['SINGLE_SERVICE'] === 'MSP') { |
|
313
|
|
|
$extType = \core\IdP::TYPE_SP; |
|
314
|
|
|
} |
|
315
|
|
|
$this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,name,country, external_db_uniquehandle) VALUES(?, ?, ?, ?, ?, ?, ?)", "sssssss", $extType, $level, $oneDest, $token, $instIdentifier, $extCountry, $externalId); |
|
316
|
|
|
$tokenList[$token] = $oneDest; |
|
317
|
|
|
} else if (func_num_args() == 6) { // string name, and country set - whole new IdP |
|
318
|
|
|
$this->databaseHandle->exec("INSERT INTO invitations (invite_fortype, invite_issuer_level, invite_dest_mail, invite_token,name,country) VALUES(?, ?, ?, ?, ?, ?)", "ssssss", $partType, $level, $oneDest, $token, $instIdentifier, $country); |
|
319
|
|
|
$tokenList[$token] = $oneDest; |
|
320
|
|
|
} else { |
|
321
|
|
|
throw new Exception("The invitation is somehow ... wrong."); |
|
322
|
|
|
} |
|
323
|
|
|
} |
|
324
|
|
|
if (count($for) != count($tokenList)) { |
|
325
|
|
|
throw new Exception("Creation of a new token failed!"); |
|
326
|
|
|
} |
|
327
|
|
|
return $tokenList; |
|
328
|
|
|
} |
|
329
|
|
|
|
|
330
|
|
|
/** |
|
331
|
|
|
* Retrieves all pending invitations for an institution or for a federation. |
|
332
|
|
|
* |
|
333
|
|
|
* @param int $idpIdentifier the identifier of the institution. If not set, returns invitations for not-yet-created insts |
|
334
|
|
|
* @return array if idp_identifier is set: an array of strings (mail addresses); otherwise an array of tuples (country;name;mail) |
|
335
|
|
|
*/ |
|
336
|
|
|
public function listPendingInvitations($idpIdentifier = 0) |
|
337
|
|
|
{ |
|
338
|
|
|
$retval = []; |
|
339
|
|
|
$invitations = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token , TIMESTAMPADD(DAY, 1, invite_created) as expiry |
|
340
|
|
|
FROM invitations |
|
341
|
|
|
WHERE cat_institution_id " . ( $idpIdentifier != 0 ? "= $idpIdentifier" : "IS NULL") . " AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0"); |
|
342
|
|
|
// SELECT -> resource, not boolean |
|
343
|
|
|
common\Logging::debug_s(4, "Retrieving pending invitations for " . ($idpIdentifier != 0 ? "IdP $idpIdentifier" : "IdPs awaiting initial creation" ) . ".\n"); |
|
344
|
|
|
while ($invitationQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $invitations)) { |
|
345
|
|
|
$retval[] = ["country" => $invitationQuery->country, "name" => $invitationQuery->name, "mail" => $invitationQuery->invite_dest_mail, "token" => $invitationQuery->invite_token, "expiry" => $invitationQuery->expiry]; |
|
346
|
|
|
} |
|
347
|
|
|
return $retval; |
|
348
|
|
|
} |
|
349
|
|
|
|
|
350
|
|
|
/** Retrieves all invitations which have expired in the last hour. |
|
351
|
|
|
* |
|
352
|
|
|
* @return array of expired invitations |
|
353
|
|
|
*/ |
|
354
|
|
|
public function listRecentlyExpiredInvitations() |
|
355
|
|
|
{ |
|
356
|
|
|
$retval = []; |
|
357
|
|
|
$invitations = $this->databaseHandle->exec("SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token |
|
358
|
|
|
FROM invitations |
|
359
|
|
|
WHERE invite_created >= TIMESTAMPADD(HOUR, -25, NOW()) AND invite_created < TIMESTAMPADD(HOUR, -24, NOW()) AND used = 0"); |
|
360
|
|
|
// SELECT -> resource, not boolean |
|
361
|
|
|
while ($expInvitationQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $invitations)) { |
|
362
|
|
|
common\Logging::debug_s(4, "Retrieving recently expired invitations (expired in last hour)\n"); |
|
363
|
|
|
if ($expInvitationQuery->cat_institution_id == NULL) { |
|
364
|
|
|
$retval[] = ["country" => $expInvitationQuery->country, "level" => $expInvitationQuery->invite_issuer_level, "name" => $expInvitationQuery->name, "mail" => $expInvitationQuery->invite_dest_mail]; |
|
365
|
|
|
} else { |
|
366
|
|
|
$retval[] = ["country" => $expInvitationQuery->country, "level" => $expInvitationQuery->invite_issuer_level, "name" => "Existing IdP", "mail" => $expInvitationQuery->invite_dest_mail]; |
|
367
|
|
|
} |
|
368
|
|
|
} |
|
369
|
|
|
return $retval; |
|
370
|
|
|
} |
|
371
|
|
|
|
|
372
|
|
|
/** |
|
373
|
|
|
* For a given persistent user identifier, returns an array of institution identifiers (not the actual objects!) for which this |
|
374
|
|
|
* user is the/a administrator and also do comparisons to the eduroam DB results. |
|
375
|
|
|
* If the federation autoregister-synced flag is set if it turns out that the eduroam DB |
|
376
|
|
|
* lists the email of the current logged-in admin as an admin of an existing CAT institution |
|
377
|
|
|
* and this institution is synced to the matching external institutuin then this admin |
|
378
|
|
|
* will be automatically added tho the institution and the 'existing' part of $this->currentInstitutions |
|
379
|
|
|
* will be updated. This identifier will also be listed to $this->currentInstitutions['resynced'] |
|
380
|
|
|
* |
|
381
|
|
|
* If the federation autoregister-new-inst flag is set and there are exeternal institututions which could be |
|
382
|
|
|
* candidated for creating them in CAT - add the identifiers of these institutuins to this->currentInstitutions[new'] |
|
383
|
|
|
* |
|
384
|
|
|
* @return array array of institution IDs |
|
385
|
|
|
*/ |
|
386
|
|
|
public function listInstitutionsByAdmin() |
|
387
|
|
|
{ |
|
388
|
|
|
$edugain = $_SESSION['eduGAIN']; |
|
389
|
|
|
// get the list of local identifers of institutions managed by this user |
|
390
|
|
|
// it will be returned as $this->currentInstitutions |
|
391
|
|
|
$this->getCurrentInstitutionsByAdmin(); |
|
392
|
|
|
if (count($this->currentInstitutions) == 0) { |
|
393
|
|
|
$this->newUser = true; |
|
394
|
|
|
} |
|
395
|
|
|
// check if selfservice_registration is set to eduGAIN - if not then return with the durrent list |
|
396
|
|
|
if (\config\ConfAssistant::CONSORTIUM['selfservice_registration'] !== 'eduGAIN') { |
|
397
|
|
|
common\Logging::debug_s(4, "selfservice_registration not set to eduGAIN\n"); |
|
398
|
|
|
return $this->currentInstitutions; |
|
399
|
|
|
} |
|
400
|
|
|
// now add additional institutions based on the external DB |
|
401
|
|
|
// proceed only if user has been authenticated fron an eduGAIN IdP |
|
402
|
|
|
if ($edugain == false) { |
|
403
|
|
|
return $this->currentInstitutions; |
|
404
|
|
|
} |
|
405
|
|
|
$email = $_SESSION['auth_email']; |
|
406
|
|
|
$externalDB = CAT::determineExternalConnection(); |
|
407
|
|
|
// get the list of identifiers in the external DB with this user listed as the admin and linked to CAT institutions |
|
408
|
|
|
$extInstList = $externalDB->listExternalEntitiesByUserEmail($email); |
|
409
|
|
|
$extInstListTmp = $extInstList; |
|
410
|
|
|
// we begin by removing entites in $extInstList which are already managed by this user and synced - |
|
411
|
|
|
// these require no further checking |
|
412
|
|
|
foreach ($extInstListTmp as $country => $extInstCountryList) { |
|
413
|
|
|
$len = count($extInstCountryList); |
|
414
|
|
|
for($i = 0; $i < $len; ++$i) { |
|
415
|
|
|
$extInst = $extInstCountryList[$i]; |
|
416
|
|
|
if ($extInst['inst_id'] != NULL && in_array($extInst['inst_id'], $this->currentInstitutions['existing'])) { |
|
417
|
|
|
unset($extInstList[$country][$i]); |
|
418
|
|
|
} |
|
419
|
|
|
} |
|
420
|
|
|
if (count($extInstList[$country]) == 0) { |
|
421
|
|
|
unset($extInstList[$country]); |
|
422
|
|
|
} |
|
423
|
|
|
} |
|
424
|
|
|
// now verify the $extInstList separately for each federation making sure |
|
425
|
|
|
// that the federation allows autoregistration |
|
426
|
|
|
foreach ($extInstList as $country => $extInstCountryList) { |
|
427
|
|
|
$fed = new Federation($country); |
|
428
|
|
|
$this->doExternalDBAutoregister($extInstCountryList, $fed); |
|
429
|
|
|
$this->doExternalDBAutoregisterNew($extInstCountryList, $fed); |
|
430
|
|
|
} |
|
431
|
|
|
// now we run tests for adding admins based on pairwise-id and entitlement |
|
432
|
|
|
$entitledCountries = $this->getUserEntitledFed(); |
|
433
|
|
|
$entitlementInst = $this->listCatInstitutionsByPairwiseId(); |
|
434
|
|
|
common\Logging::debug_s(4, $entitlementInst, "entitlementInst\n", "\n"); |
|
435
|
|
|
foreach ($entitlementInst as $country => $instList) { |
|
436
|
|
|
if (!in_array($country, $entitledCountries)) { |
|
437
|
|
|
continue; |
|
438
|
|
|
} |
|
439
|
|
|
$fed = new Federation($country); |
|
440
|
|
|
$this->doEntitlementAutoregister($instList, $fed); |
|
441
|
|
|
} |
|
442
|
|
|
$_SESSION['entitledIdPs'] = array_column($this->currentInstitutions['entitlement'], 0); |
|
443
|
|
|
$_SESSION['resyncedIdPs'] = $this->currentInstitutions['resynced']; |
|
444
|
|
|
$_SESSION['newIdPs'] = $this->currentInstitutions['new']; |
|
445
|
|
|
common\Logging::debug_s(4, $this->currentInstitutions, "currentInstitutions\n", "\n"); |
|
446
|
|
|
return $this->currentInstitutions; |
|
447
|
|
|
} |
|
448
|
|
|
|
|
449
|
|
|
/** |
|
450
|
|
|
* Handle auto-registration of admin for CAT institutions which are synced to |
|
451
|
|
|
* eduroam DB institutions which have the current admin listed |
|
452
|
|
|
* The method verifies that the federation allows auto-registration |
|
453
|
|
|
* |
|
454
|
|
|
* @param array $extInstCountryList - list of eduroam DB candidate institutions |
|
455
|
|
|
* @param object $fed - the Federation object |
|
456
|
|
|
*/ |
|
457
|
|
|
private function doExternalDBAutoregister($extInstCountryList, $fed) { |
|
458
|
|
|
$userId = $_SESSION['user']; |
|
|
|
|
|
|
459
|
|
|
$email = $_SESSION['auth_email']; |
|
|
|
|
|
|
460
|
|
|
$autoSyncedFlag = $fed->getAttributes('fed:autoregister-synced'); |
|
461
|
|
|
if ($autoSyncedFlag == []) { |
|
462
|
|
|
return; |
|
463
|
|
|
} |
|
464
|
|
|
foreach ($extInstCountryList as $extInst) { |
|
465
|
|
|
common\Logging::debug_s(4, "Testing ".$extInst['external_db_id']."\n"); |
|
466
|
|
|
if ($extInst['inst_id'] == null) { |
|
467
|
|
|
// this institution is not synced, skip |
|
468
|
|
|
continue; |
|
469
|
|
|
} |
|
470
|
|
|
// is institution synced, if so we add this admin if the federation allows |
|
471
|
|
|
common\Logging::debug_s(4, "It is synced\n"); |
|
472
|
|
|
$this->currentInstitutions['resynced'][] = $extInst['inst_id']; |
|
473
|
|
|
} |
|
474
|
|
|
} |
|
475
|
|
|
|
|
476
|
|
|
/** |
|
477
|
|
|
* Handle auto-creation of new CAT institutions which match |
|
478
|
|
|
* eduroam DB institutions which have the current admin listed |
|
479
|
|
|
* The method verifies that the federation allows auto-cereation |
|
480
|
|
|
* |
|
481
|
|
|
* @param array $extInstCountryList - list of eduroam DB candidate institutions |
|
482
|
|
|
* @param object $fed - the Federation object |
|
483
|
|
|
*/ |
|
484
|
|
|
private function doExternalDBAutoregisterNew($extInstCountryList, $fed) { |
|
485
|
|
|
$newInstFlag = $fed->getAttributes('fed:autoregister-new-inst'); |
|
486
|
|
|
if ($newInstFlag == []) { |
|
487
|
|
|
return; |
|
488
|
|
|
} |
|
489
|
|
|
foreach ($extInstCountryList as $extInst) { |
|
490
|
|
|
common\Logging::debug_s(4, "Testing ".$extInst['external_db_id']." for potential new inst\n"); |
|
491
|
|
|
if ($extInst['inst_id'] != null) { // there alreay exeists a CAT institution synced to this one |
|
492
|
|
|
continue; |
|
493
|
|
|
} |
|
494
|
|
|
$country = strtoupper($fed->tld); |
|
495
|
|
|
// now run checks against creating dupplicates in CAT DB |
|
496
|
|
|
$disectedNames = ExternalEduroamDBData::dissectCollapsedInstitutionNames($extInst['name']); |
|
497
|
|
|
$names = $disectedNames['joint']; |
|
498
|
|
|
$realms = ExternalEduroamDBData::dissectCollapsedInstitutionRealms($extInst['realm']); |
|
499
|
|
|
$foundMatch = $this->checkForSimilarInstitutions($names, $realms); |
|
500
|
|
|
common\Logging::debug_s(4, $foundMatch, "checkForSimilarInstitutions returned: ","\n"); |
|
501
|
|
|
if ($foundMatch == 0) { |
|
502
|
|
|
$this->currentInstitutions['new'][] = [$extInst['external_db_id'], $disectedNames['perlang'], $country]; |
|
503
|
|
|
} |
|
504
|
|
|
} |
|
505
|
|
|
} |
|
506
|
|
|
|
|
507
|
|
|
/** |
|
508
|
|
|
* Handle auto-registration of admin for CAT institutions |
|
509
|
|
|
* based on data from eduGAIN login |
|
510
|
|
|
* The method verifies that the federation allows this type of auto-registration |
|
511
|
|
|
* |
|
512
|
|
|
* @param object $fed - the Federation object |
|
513
|
|
|
*/ |
|
514
|
|
|
private function doEntitlementAutoregister($instList, $fed) { |
|
515
|
|
|
$useEntitlementFlag = $fed->getAttributes('fed:autoregister-entitlement'); |
|
516
|
|
|
if ($useEntitlementFlag == []) { |
|
517
|
|
|
return; |
|
518
|
|
|
} |
|
519
|
|
|
$country = strtoupper($fed->tld); |
|
520
|
|
|
foreach ($instList as $instId) { |
|
521
|
|
|
if (!in_array($instId, $this->currentInstitutions['existing'])) { |
|
522
|
|
|
$this->currentInstitutions['entitlement'][] = [$instId, $country]; |
|
523
|
|
|
} |
|
524
|
|
|
} |
|
525
|
|
|
|
|
526
|
|
|
} |
|
527
|
|
|
|
|
528
|
|
|
/** |
|
529
|
|
|
* Generate a list of externalDB institutions for which the admin is entitled |
|
530
|
|
|
* based on the eduPersonEntitlement setting and scope from pairwise-id |
|
531
|
|
|
* The |
|
532
|
|
|
* @return array indexed by countries |
|
533
|
|
|
*/ |
|
534
|
|
|
private function listCatInstitutionsByPairwiseId() { |
|
535
|
|
|
if (!isset(\config\ConfAssistant::CONSORTIUM['entitlement'])) { |
|
536
|
|
|
return []; |
|
537
|
|
|
} |
|
538
|
|
|
// first check if pairwise-id is set |
|
539
|
|
|
$userId = $_SESSION['user']; |
|
540
|
|
|
if (substr($userId, 0, 12) !== 'pairwise-id:') { |
|
541
|
|
|
return []; |
|
542
|
|
|
} |
|
543
|
|
|
|
|
544
|
|
|
// next check if entitlement is setand matches our expectations |
|
545
|
|
|
if (!isset($_SESSION['entitlement'])) { |
|
546
|
|
|
return []; |
|
547
|
|
|
} |
|
548
|
|
|
|
|
549
|
|
|
// get realm from pariwise-id |
|
550
|
|
|
if (preg_match('/^pairwise-id:[^@]+@([^!]+)!/', $userId, $matches) == 0) { |
|
551
|
|
|
return []; |
|
552
|
|
|
} |
|
553
|
|
|
|
|
554
|
|
|
$userRealm = $matches[1]; |
|
555
|
|
|
common\Logging::debug_s(4, $userRealm, "userRealm:", "\n"); |
|
556
|
|
|
// list CAT inst_id and country from the profile |
|
557
|
|
|
$query = "SELECT DISTINCT profile.inst_id,country FROM profile JOIN institution on profile.inst_id = institution.inst_id WHERE realm LIKE '%@$userRealm' OR realm LIKE '%@%.$userRealm'"; |
|
558
|
|
|
$institutions = $this->databaseHandle->exec($query); |
|
559
|
|
|
$catInstList = $institutions->fetch_all(); |
|
560
|
|
|
$returnarray = []; |
|
561
|
|
|
foreach ($catInstList as $inst) { |
|
562
|
|
|
$country = $inst[1]; |
|
563
|
|
|
if (!isset($returnarray[$country])) { |
|
564
|
|
|
$returnarray[$country] = []; |
|
565
|
|
|
} |
|
566
|
|
|
$returnarray[$country][] = $inst[0]; |
|
567
|
|
|
} |
|
568
|
|
|
return $returnarray; |
|
569
|
|
|
} |
|
570
|
|
|
|
|
571
|
|
|
/** |
|
572
|
|
|
* Get the list of eduroam countries that the user could potentially auto-register |
|
573
|
|
|
* based on eduPersonEntitlement. The list of countries is based on matching between |
|
574
|
|
|
* the eduGAIN federation where the eduGAIN IdP is registered and the counters that this federation serves |
|
575
|
|
|
* Before passing on the list it is checked if particular countries allow entitlement-based |
|
576
|
|
|
* autoregistration |
|
577
|
|
|
* |
|
578
|
|
|
* @return array indexed by countries |
|
579
|
|
|
*/ |
|
580
|
|
|
private function getUserEntitledFed() { |
|
581
|
|
|
if (!isset($_SESSION['entitlement'])) { |
|
582
|
|
|
return []; |
|
583
|
|
|
} |
|
584
|
|
|
$entitledCountries = []; |
|
585
|
|
|
$countries = $this->databaseHandle->exec("SELECT country FROM edugain WHERE reg_auth = ?", 's', $_SESSION['eduGAIN']); |
|
586
|
|
|
$countryList = $countries->fetch_all(); |
|
587
|
|
|
$countriesTmp = []; |
|
|
|
|
|
|
588
|
|
|
foreach ($countryList as $country) { |
|
589
|
|
|
$requiredEntitlement = NULL; |
|
590
|
|
|
$countryCode = $country[0]; |
|
591
|
|
|
$fed = new Federation($countryCode); |
|
592
|
|
|
$autoreg = $fed->getAttributes('fed:autoregister-entitlement'); |
|
593
|
|
|
$entitlementVal = $fed->getAttributes('fed:entitlement-attr'); |
|
594
|
|
|
if (isset($autoreg[0]['value']) && $autoreg[0]['value'] == 'on') { |
|
595
|
|
|
$requiredEntitlement = isset($entitlementVal[0]['value']) ? $entitlementVal[0]['value'] : \config\ConfAssistant::CONSORTIUM['entitlement']; |
|
596
|
|
|
} |
|
597
|
|
|
common\Logging::debug_s(4, $requiredEntitlement, "$countryCode requiredEntitlement\n", "\n"); |
|
598
|
|
|
if (in_array($requiredEntitlement, $_SESSION['entitlement'])) { |
|
599
|
|
|
$entitledCountries[] = $countryCode; |
|
600
|
|
|
} |
|
601
|
|
|
} |
|
602
|
|
|
common\Logging::debug_s(4, $entitledCountries, "entitledCountries:\n", "\n"); |
|
603
|
|
|
return $entitledCountries; |
|
604
|
|
|
} |
|
605
|
|
|
|
|
606
|
|
|
/** |
|
607
|
|
|
* Tests if the institution with these identifier does not yet exist in CAT. |
|
608
|
|
|
* This is done by testing the admins "new" institutions, this way we also make sure |
|
609
|
|
|
* that this admin is actually also allowed to create the new one |
|
610
|
|
|
* |
|
611
|
|
|
* @return int 1 or 0. 1 means we are free to create the inst. |
|
612
|
|
|
*/ |
|
613
|
|
|
|
|
614
|
|
|
public function checkForCatMatch($extId, $ROid) { |
|
615
|
|
|
$this->listInstitutionsByAdmin(); |
|
616
|
|
|
foreach ($this->currentInstitutions['new'] as $newInst) { |
|
617
|
|
|
if ($extId == $newInst[0] && $ROid == strtoupper($newInst[2]).'01') { |
|
618
|
|
|
return 0; |
|
619
|
|
|
} |
|
620
|
|
|
} |
|
621
|
|
|
return 1; |
|
622
|
|
|
} |
|
623
|
|
|
|
|
624
|
|
|
/** |
|
625
|
|
|
* get the list of current institutions of the given admin |
|
626
|
|
|
* |
|
627
|
|
|
* This method does not rerurn anything but sets $this->currentInstitutions |
|
628
|
|
|
* it only fillsh the 'existing' block, leaving the other two for other methods |
|
629
|
|
|
* to deal with |
|
630
|
|
|
*/ |
|
631
|
|
|
private function getCurrentInstitutionsByAdmin() { |
|
632
|
|
|
$returnarray = [ |
|
633
|
|
|
'existing' => [], |
|
634
|
|
|
'resynced' => [], |
|
635
|
|
|
'new' => [], |
|
636
|
|
|
'entitlement' => [] |
|
637
|
|
|
]; |
|
638
|
|
|
$userId = $_SESSION['user']; |
|
639
|
|
|
// get the list of local identifers of institutions managed by this user |
|
640
|
|
|
$institutions = $this->databaseHandle->exec("SELECT ownership.institution_id as inst_id FROM ownership WHERE user_id = ? ORDER BY institution_id", "s", $userId); |
|
641
|
|
|
// SELECT -> resource, not boolean |
|
642
|
|
|
$catInstList = $institutions->fetch_all(); |
|
643
|
|
|
foreach ($catInstList as $inst) { |
|
644
|
|
|
$returnarray['existing'][] = $inst[0]; |
|
645
|
|
|
} |
|
646
|
|
|
$this->currentInstitutions = $returnarray; |
|
647
|
|
|
} |
|
648
|
|
|
|
|
649
|
|
|
/** |
|
650
|
|
|
* given arrays of realms and names check if there already are institutions in CAT that |
|
651
|
|
|
* could be a match - this is for ellimination and against creating duplicates |
|
652
|
|
|
* still this is not perfect, no realms given and institutions with a slightly different |
|
653
|
|
|
* name will return no-match and thus open possibility for dupplicates |
|
654
|
|
|
* |
|
655
|
|
|
* @param array $namesToTest |
|
656
|
|
|
* @param array $realmsToTest |
|
657
|
|
|
* @return int - 1 - a match was found, 0 - no match found |
|
658
|
|
|
*/ |
|
659
|
|
|
private function checkForSimilarInstitutions($namesToTest, $realmsToTest) { |
|
660
|
|
|
//generate a list of all existing realms |
|
661
|
|
|
\core\common\Logging::debug_s(4, $namesToTest, "Testing names:\n", "\n"); |
|
662
|
|
|
\core\common\Logging::debug_s(4, $realmsToTest, "Testing realms:\n", "\n"); |
|
663
|
|
|
$realmsList = []; |
|
664
|
|
|
$query = 'SELECT DISTINCT realm FROM profile'; |
|
665
|
|
|
$realmsResult = $this->databaseHandle->exec($query); |
|
666
|
|
|
while ($anonId = $realmsResult->fetch_row()) { |
|
667
|
|
|
$realmsList[] = mb_strtolower(preg_replace('/^.*@/', '', $anonId[0]), 'UTF-8'); |
|
668
|
|
|
} |
|
669
|
|
|
// now test realms |
|
670
|
|
|
$results = array_intersect($realmsToTest, $realmsList); |
|
671
|
|
|
\core\common\Logging::debug_s(4, $results, "Realms compare result\n", "\n"); |
|
672
|
|
|
if (count($results) !== 0) { |
|
673
|
|
|
return 1; |
|
674
|
|
|
} |
|
675
|
|
|
|
|
676
|
|
|
// generate a list of all institution names |
|
677
|
|
|
$query = "SELECT DISTINCT CONVERT(option_value USING utf8mb4) FROM institution_option WHERE option_name='general:instname'"; |
|
678
|
|
|
$namesResult = $this->databaseHandle->exec($query); |
|
679
|
|
|
$namesList = []; |
|
680
|
|
|
while ($name = $namesResult->fetch_row()) { |
|
681
|
|
|
$namesList[] = mb_strtolower($name[0], 'UTF-8'); |
|
682
|
|
|
} |
|
683
|
|
|
|
|
684
|
|
|
// now test names |
|
685
|
|
|
$results = array_intersect($namesToTest, $namesList); |
|
686
|
|
|
\core\common\Logging::debug_s(4, $results, "Realms compare result\n", "\n"); |
|
687
|
|
|
if (count($results) !== 0) { |
|
688
|
|
|
return 1; |
|
689
|
|
|
} |
|
690
|
|
|
return 0; |
|
691
|
|
|
} |
|
692
|
|
|
|
|
693
|
|
|
/** |
|
694
|
|
|
* read the last login date of given user identifier |
|
695
|
|
|
* |
|
696
|
|
|
* @param string $user |
|
697
|
|
|
* @return string NULL - the date last seen or NULL if not recorded yet |
|
698
|
|
|
*/ |
|
699
|
|
|
public function getAdminLastAuth($user) { |
|
700
|
|
|
$truncatedUser = substr($user,0,999); |
|
701
|
|
|
$result = $this->databaseHandle->exec("SELECT DATE(last_login) FROM admin_logins WHERE user_id='$truncatedUser'"); |
|
702
|
|
|
if ($result->num_rows == 1) { |
|
703
|
|
|
$date = $result ->fetch_row()[0]; |
|
704
|
|
|
return $date; |
|
705
|
|
|
} |
|
706
|
|
|
return NULL; |
|
707
|
|
|
} |
|
708
|
|
|
} |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths