OAuth2   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 540
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 62
eloc 305
c 0
b 0
f 0
dl 0
loc 540
rs 3.44

13 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 5 2
A getSignInURL() 0 3 1
A log() 0 4 2
B getValuesByKey() 0 25 7
A install() 0 7 1
A getLogoutUrl() 0 9 2
B updateUserUrls() 0 31 11
A isFirstLoginAfterAuthSource() 0 31 3
B getProvider() 0 48 8
A __construct() 0 57 1
A updateUser() 0 52 4
A mapUserStatusFromResponse() 0 33 6
C getUserInfo() 0 114 14

How to fix   Complexity   

Complex Class

Complex classes like OAuth2 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OAuth2, and based on these observations, apply Extract Interface, too.

1
<?php
2
/* For license terms, see /license.txt */
3
4
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
5
use Chamilo\CoreBundle\Entity\TrackELogin;
6
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
7
use League\OAuth2\Client\Provider\AbstractProvider;
8
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
9
use League\OAuth2\Client\Provider\GenericProvider;
10
use League\OAuth2\Client\Token\AccessToken;
11
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
12
13
/**
14
 * OAuth2 plugin class.
15
 *
16
 * @author Sébastien Ducoulombier <[email protected]>
17
 * inspired by AzureActiveDirectory plugin class from Angel Fernando Quiroz Campos <[email protected]>
18
 *
19
 * @package chamilo.plugin.oauth2
20
 */
21
class OAuth2 extends Plugin
22
{
23
    use ArrayAccessorTrait;
24
25
    public const SETTING_ENABLE = 'enable';
26
27
    public const SETTING_FORCE_REDIRECT = 'force_redirect';
28
    public const SETTING_SKIP_FORCE_REDIRECT_IN = 'skip_force_redirect_in';
29
30
    public const SETTING_CLIENT_ID = 'client_id';
31
    public const SETTING_CLIENT_SECRET = 'client_secret';
32
33
    public const SETTING_AUTHORIZE_URL = 'authorize_url';
34
    public const SETTING_SCOPES = 'scopes';
35
    public const SETTING_SCOPE_SEPARATOR = 'scope_separator';
36
37
    public const SETTING_ACCESS_TOKEN_URL = 'access_token_url';
38
    public const SETTING_ACCESS_TOKEN_METHOD = 'access_token_method';
39
    // const SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID = 'access_token_resource_owner_id';
40
41
    public const SETTING_RESOURCE_OWNER_DETAILS_URL = 'resource_owner_details_url';
42
43
    public const SETTING_RESPONSE_ERROR = 'response_error';
44
    public const SETTING_RESPONSE_CODE = 'response_code';
45
    public const SETTING_RESPONSE_RESOURCE_OWNER_ID = 'response_resource_owner_id';
46
47
    public const SETTING_UPDATE_USER_INFO = 'update_user_info';
48
    public const SETTING_CREATE_NEW_USERS = 'create_new_users';
49
    public const SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME = 'response_resource_owner_firstname';
50
    public const SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME = 'response_resource_owner_lastname';
51
    public const SETTING_RESPONSE_RESOURCE_OWNER_STATUS = 'response_resource_owner_status';
52
    public const SETTING_RESPONSE_RESOURCE_OWNER_TEACHER_STATUS = 'response_resource_owner_teacher_status';
53
    public const SETTING_RESPONSE_RESOURCE_OWNER_SESSADMIN_STATUS = 'response_resource_owner_sessadmin_status';
54
    public const SETTING_RESPONSE_RESOURCE_OWNER_DRH_STATUS = 'response_resource_owner_drh_status';
55
    public const SETTING_RESPONSE_RESOURCE_OWNER_STUDENT_STATUS = 'response_resource_owner_student_status';
56
    public const SETTING_RESPONSE_RESOURCE_OWNER_ANON_STATUS = 'response_resource_owner_anon_status';
57
    public const SETTING_RESPONSE_RESOURCE_OWNER_EMAIL = 'response_resource_owner_email';
58
    public const SETTING_RESPONSE_RESOURCE_OWNER_USERNAME = 'response_resource_owner_username';
59
60
    public const SETTING_RESPONSE_RESOURCE_OWNER_URLS = 'response_resource_owner_urls';
61
62
    public const SETTING_LOGOUT_URL = 'logout_url';
63
64
    public const SETTING_BLOCK_NAME = 'block_name';
65
66
    public const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable';
67
    public const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name';
68
69
    public const SETTING_ALLOW_THIRD_PARTY_LOGIN = 'allow_third_party_login';
70
71
    public const EXTRA_FIELD_OAUTH2_ID = 'oauth2_id';
72
73
    private const DEBUG = false;
74
75
    protected function __construct()
76
    {
77
        parent::__construct(
78
            '0.1',
79
            'Sébastien Ducoulombier',
80
            [
81
                self::SETTING_ENABLE => 'boolean',
82
83
                self::SETTING_FORCE_REDIRECT => 'boolean',
84
                self::SETTING_SKIP_FORCE_REDIRECT_IN => 'text',
85
86
                self::SETTING_CLIENT_ID => 'text',
87
                self::SETTING_CLIENT_SECRET => 'text',
88
89
                self::SETTING_AUTHORIZE_URL => 'text',
90
                self::SETTING_SCOPES => 'text',
91
                self::SETTING_SCOPE_SEPARATOR => 'text',
92
93
                self::SETTING_ACCESS_TOKEN_URL => 'text',
94
                self::SETTING_ACCESS_TOKEN_METHOD => [
95
                    'type' => 'select',
96
                    'options' => [
97
                        AbstractProvider::METHOD_POST => 'POST',
98
                        AbstractProvider::METHOD_GET => 'GET',
99
                    ],
100
                ],
101
                // self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID => 'text',
102
103
                self::SETTING_RESOURCE_OWNER_DETAILS_URL => 'text',
104
105
                self::SETTING_RESPONSE_ERROR => 'text',
106
                self::SETTING_RESPONSE_CODE => 'text',
107
                self::SETTING_RESPONSE_RESOURCE_OWNER_ID => 'text',
108
109
                self::SETTING_UPDATE_USER_INFO => 'boolean',
110
                self::SETTING_CREATE_NEW_USERS => 'boolean',
111
                self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME => 'text',
112
                self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME => 'text',
113
                self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS => 'text',
114
                self::SETTING_RESPONSE_RESOURCE_OWNER_TEACHER_STATUS => 'text',
115
                self::SETTING_RESPONSE_RESOURCE_OWNER_SESSADMIN_STATUS => 'text',
116
                self::SETTING_RESPONSE_RESOURCE_OWNER_DRH_STATUS => 'text',
117
                self::SETTING_RESPONSE_RESOURCE_OWNER_STUDENT_STATUS => 'text',
118
                self::SETTING_RESPONSE_RESOURCE_OWNER_ANON_STATUS => 'text',
119
                self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL => 'text',
120
                self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME => 'text',
121
122
                self::SETTING_RESPONSE_RESOURCE_OWNER_URLS => 'text',
123
124
                self::SETTING_LOGOUT_URL => 'text',
125
126
                self::SETTING_BLOCK_NAME => 'text',
127
128
                self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean',
129
                self::SETTING_MANAGEMENT_LOGIN_NAME => 'text',
130
131
                self::SETTING_ALLOW_THIRD_PARTY_LOGIN => 'boolean',
132
            ]
133
        );
134
    }
135
136
    /**
137
     * Instance the plugin.
138
     *
139
     * @staticvar null $result
140
     *
141
     * @return $this
142
     */
143
    public static function create(): OAuth2
144
    {
145
        static $result = null;
146
147
        return $result ?: $result = new self();
148
    }
149
150
    public function getProvider(): GenericProvider
151
    {
152
        $redirectUri = api_get_path(WEB_PLUGIN_PATH).'oauth2/src/callback.php';
153
        // In cases not precisely defined yet, this alternative version might be necessary - see BT#20611
154
        //$redirectUri = api_get_path(WEB_PATH).'authorization-code/callback';
155
        $options = [
156
            'clientId' => $this->get(self::SETTING_CLIENT_ID),
157
            'clientSecret' => $this->get(self::SETTING_CLIENT_SECRET),
158
            'redirectUri' => $redirectUri,
159
            'urlAuthorize' => $this->get(self::SETTING_AUTHORIZE_URL),
160
            'urlResourceOwnerDetails' => $this->get(self::SETTING_RESOURCE_OWNER_DETAILS_URL),
161
        ];
162
163
        if ('' === $scopeSeparator = (string) $this->get(self::SETTING_SCOPE_SEPARATOR)) {
164
            $scopeSeparator = ' ';
165
        }
166
167
        $options['scopeSeparator'] = $scopeSeparator;
168
169
        if ('' !== $scopes = (string) $this->get(self::SETTING_SCOPES)) {
170
            $options['scopes'] = explode($scopeSeparator, $scopes);
171
        }
172
173
        if ('' !== $urlAccessToken = (string) $this->get(self::SETTING_ACCESS_TOKEN_URL)) {
174
            $options['urlAccessToken'] = $urlAccessToken;
175
        }
176
177
        if ('' !== $accessTokenMethod = (string) $this->get(self::SETTING_ACCESS_TOKEN_METHOD)) {
178
            $options['accessTokenMethod'] = $accessTokenMethod;
179
        }
180
181
//        if ('' !== $accessTokenResourceOwnerId = (string) $this->get(self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID)) {
182
//            $options['accessTokenResourceOwnerId'] = $accessTokenResourceOwnerId;
183
//        }
184
185
        if ('' !== $responseError = (string) $this->get(self::SETTING_RESPONSE_ERROR)) {
186
            $options['responseError'] = $responseError;
187
        }
188
189
        if ('' !== $responseCode = (string) $this->get(self::SETTING_RESPONSE_CODE)) {
190
            $options['responseCode'] = $responseCode;
191
        }
192
193
        if ('' !== $responseResourceOwnerId = (string) $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)) {
194
            $options['responseResourceOwnerId'] = $responseResourceOwnerId;
195
        }
196
197
        return new GenericProvider($options);
198
    }
199
200
    /**
201
     * @throws IdentityProviderException
202
     *
203
     * @return array user information, as returned by api_get_user_info(userId)
204
     */
205
    public function getUserInfo(GenericProvider $provider, AccessToken $accessToken): array
206
    {
207
        $url = $provider->getResourceOwnerDetailsUrl($accessToken);
208
        $request = $provider->getAuthenticatedRequest($provider::METHOD_GET, $url, $accessToken);
209
        $response = $provider->getParsedResponse($request);
210
        $this->log('response', print_r($response, true));
211
212
        if (false === is_array($response)) {
213
            $this->log('invalid response', print_r($response, true));
214
            throw new UnexpectedValueException($this->get_lang('InvalidJsonReceivedFromProvider'));
215
        }
216
        $resourceOwnerId = $this->getValueByKey(
217
            $response,
218
            $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)
219
        );
220
        if (empty($resourceOwnerId)) {
221
            $this->log('missing setting', 'response_resource_owner_id');
222
            throw new RuntimeException($this->get_lang('WrongResponseResourceOwnerId'));
223
        }
224
        $this->log('response resource owner id', $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID));
225
        $extraFieldValue = new ExtraFieldValue('user');
226
        $result = $extraFieldValue->get_item_id_from_field_variable_and_field_value(
227
            self::EXTRA_FIELD_OAUTH2_ID,
228
            $resourceOwnerId
229
        );
230
        if (false === $result) {
231
            $this->log('user not found', "extrafield 'oauth2_id' with value '$resourceOwnerId'");
232
233
            $username = $this->getValueByKey(
234
                $response,
235
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME),
236
                'oauth2user_'.$resourceOwnerId
237
            );
238
239
            $userInfo = api_get_user_info_from_username($username);
240
241
            if (false !== $userInfo && !empty($userInfo['id']) && 'platform' === $userInfo['auth_source']) {
242
                $this->log('platform user exists', print_r($userInfo, true));
243
244
                $userId = $userInfo['id'];
245
            } else {
246
                // authenticated user not found in internal database
247
                if ('true' !== $this->get(self::SETTING_CREATE_NEW_USERS)) {
248
                    $this->log('exception', 'create_new_users setting is disabled');
249
                    $message = sprintf(
250
                        $this->get_lang('NoUserAccountAndUserCreationNotAllowed'),
251
                        Display::encrypted_mailto_link(api_get_setting('emailAdministrator'))
252
                    );
253
                    throw new RuntimeException($message);
254
                }
255
256
                require_once __DIR__.'/../../../main/auth/external_login/functions.inc.php';
257
258
                $firstName = $this->getValueByKey(
259
                    $response,
260
                    $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME),
261
                    $this->get_lang('DefaultFirstname')
262
                );
263
                $lastName = $this->getValueByKey(
264
                    $response,
265
                    $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME),
266
                    $this->get_lang('DefaultLastname')
267
                );
268
                $status = $this->mapUserStatusFromResponse($response);
269
                $email = $this->getValueByKey(
270
                    $response,
271
                    $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL),
272
                    'oauth2user_'.$resourceOwnerId.'@'.(gethostname() or 'localhost')
273
                );
274
275
                $userInfo = [
276
                    'firstname' => $firstName,
277
                    'lastname' => $lastName,
278
                    'status' => $status,
279
                    'email' => $email,
280
                    'username' => $username,
281
                    'auth_source' => 'oauth2',
282
                ];
283
                $userId = external_add_user($userInfo);
284
                if (false === $userId) {
285
                    $this->log('user not created', print_r($userInfo, true));
286
                    throw new RuntimeException($this->get_lang('FailedUserCreation'));
287
                }
288
                $this->log('user created', (string) $userId);
289
            }
290
291
            $this->updateUser($userId, $response);
292
            // Not checking function update_extra_field_value return value because not reliable
293
            UserManager::update_extra_field_value($userId, self::EXTRA_FIELD_OAUTH2_ID, $resourceOwnerId);
294
            $this->updateUserUrls($userId, $response);
295
        } else {
296
            $this->log('user found', "extrafield 'oauth2_id' with value '$resourceOwnerId'");
297
            // authenticated user found in internal database
298
            if (is_array($result) and array_key_exists('item_id', $result)) {
299
                $userId = $result['item_id'];
300
            } else {
301
                $userId = $result;
302
            }
303
            if ('true' === $this->get(self::SETTING_UPDATE_USER_INFO)) {
304
                $this->updateUser($userId, $response);
305
                $this->updateUserUrls($userId, $response);
306
307
                Event::addEvent(LOG_USER_UPDATE, LOG_USER_ID, $userId);
0 ignored issues
show
Bug introduced by
The method addEvent() does not exist on Event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

307
                Event::/** @scrutinizer ignore-call */ 
308
                       addEvent(LOG_USER_UPDATE, LOG_USER_ID, $userId);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
308
            }
309
        }
310
        $userInfo = api_get_user_info($userId);
311
        if (empty($userInfo)) {
312
            $this->log('user info not found', (string) $userId);
313
            throw new LogicException($this->get_lang('InternalErrorCannotGetUserInfo'));
314
        }
315
316
        $this->log('user info', print_r($userInfo, true));
317
318
        return $userInfo;
319
    }
320
321
    public function getSignInURL(): string
322
    {
323
        return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php';
324
        // In cases not precisely defined yet, this alternative version might be necessary - see BT#20611
325
        //return api_get_path(WEB_PATH).'authorization-code/callback';
326
    }
327
328
    public function getLogoutUrl(): string
329
    {
330
        $token = ChamiloSession::read('oauth2AccessToken');
331
        $idToken = !empty($token['id_token']) ? $token['id_token'] : null;
332
333
        return $this->get(self::SETTING_LOGOUT_URL).'?'.http_build_query(
334
            [
335
                'id_token_hint' => $idToken,
336
                'post_logout_redirect_uri' => api_get_path(WEB_PATH),
337
            ]
338
        );
339
    }
340
341
    /**
342
     * Create extra fields for user when installing.
343
     */
344
    public function install()
345
    {
346
        UserManager::create_extra_field(
347
            self::EXTRA_FIELD_OAUTH2_ID,
348
            ExtraField::FIELD_TYPE_TEXT,
349
            $this->get_lang('OAuth2Id'),
350
            ''
351
        );
352
    }
353
354
    public static function isFirstLoginAfterAuthSource(int $userId): bool
355
    {
356
        $em = Database::getManager();
357
358
        $lastLogin = $em
359
            ->getRepository(TrackELogin::class)
360
            ->findOneBy(
361
                ['loginUserId' => $userId],
362
                ['loginDate' => 'DESC']
363
            )
364
        ;
365
366
        if (!$lastLogin) {
367
            return false;
368
        }
369
370
        $objExtraField = new ExtraField('user');
371
        $field = $objExtraField->getHandlerEntityByFieldVariable(self::EXTRA_FIELD_OAUTH2_ID);
372
373
        $fieldValue = $em
374
            ->getRepository(ExtraFieldValues::class)
375
            ->findOneBy(
376
                ['itemId' => $userId, 'field' => $field]
377
            )
378
        ;
379
380
        if (!$fieldValue) {
381
            return false;
382
        }
383
384
        return $fieldValue->getCreatedAt() >= $lastLogin->getLoginDate();
385
    }
386
387
    private function mapUserStatusFromResponse(array $response, int $defaultStatus = STUDENT): int
388
    {
389
        $status = $this->getValueByKey(
390
            $response,
391
            $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS),
392
            $defaultStatus
393
        );
394
395
        $responseStatus = [];
396
397
        if ($teacherStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_TEACHER_STATUS)) {
398
            $responseStatus[COURSEMANAGER] = $teacherStatus;
399
        }
400
401
        if ($sessAdminStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_SESSADMIN_STATUS)) {
402
            $responseStatus[SESSIONADMIN] = $sessAdminStatus;
403
        }
404
405
        if ($drhStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_DRH_STATUS)) {
406
            $responseStatus[DRH] = $drhStatus;
407
        }
408
409
        if ($studentStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_STUDENT_STATUS)) {
410
            $responseStatus[STUDENT] = $studentStatus;
411
        }
412
413
        if ($anonStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ANON_STATUS)) {
414
            $responseStatus[ANONYMOUS] = $anonStatus;
415
        }
416
417
        $map = array_flip($responseStatus);
418
419
        return $map[$status] ?? $status;
420
    }
421
422
    /**
423
     * Extends ArrayAccessorTrait::getValueByKey to return a list of values
424
     * $key can contain wild card character *
425
     * It will be replaced by 0, 1, 2 and so on as long as the resulting key exists in $data
426
     * This is a recursive function, allowing for more than one occurrence of the wild card character.
427
     */
428
    private function getValuesByKey(array $data, string $key, array $default = []): array
429
    {
430
        if (!is_string($key) || empty($key) || !count($data)) {
431
            return $default;
432
        }
433
        $pos = strpos($key, '*');
434
        if ($pos === false) {
435
            $value = $this->getValueByKey($data, $key);
436
437
            return is_null($value) ? [] : [$value];
438
        }
439
        $values = [];
440
        $beginning = substr($key, 0, $pos);
441
        $remaining = substr($key, $pos + 1);
442
        $index = 0;
443
        do {
444
            $newValues = $this->getValuesByKey(
445
                $data,
446
                $beginning.$index.$remaining
447
            );
448
            $values = array_merge($values, $newValues);
449
            $index++;
450
        } while ($newValues);
451
452
        return $values;
453
    }
454
455
    /**
456
     * @throws Exception
457
     */
458
    private function updateUser($userId, $response)
459
    {
460
        $user = UserManager::getRepository()->find($userId);
461
        $user->setFirstname(
462
            $this->getValueByKey(
463
                $response,
464
                $this->get(
465
                    self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME
466
                ),
467
                $user->getFirstname()
468
            )
469
        );
470
        $user->setLastname(
471
            $this->getValueByKey(
472
                $response,
473
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME),
474
                $user->getLastname()
475
            )
476
        );
477
        $user->setUserName(
478
            $this->getValueByKey(
479
                $response,
480
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME),
481
                $user->getUsername()
482
            )
483
        );
484
        $user->setEmail(
485
            $this->getValueByKey(
486
                $response,
487
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL),
488
                $user->getEmail()
489
            )
490
        );
491
        $status = $this->mapUserStatusFromResponse(
492
            $response,
493
            $user->getStatus()
494
        );
495
        $user->setStatus($status);
496
        $user->setAuthSource('oauth2');
497
        $configFilePath = __DIR__.'/../config.php';
498
        if (file_exists($configFilePath)) {
499
            require_once $configFilePath;
500
            $functionName = 'oauth2UpdateUserFromResourceOwnerDetails';
501
            if (function_exists($functionName)) {
502
                $functionName($response, $user);
503
            }
504
        }
505
506
        try {
507
            UserManager::getManager()->updateUser($user);
508
        } catch (UniqueConstraintViolationException $exception) {
509
            throw new Exception(get_lang('UserNameUsedTwice'));
510
        }
511
    }
512
513
    /**
514
     * Updates the Access URLs associated to a user
515
     * according to the OAuth2 server response resource owner
516
     * if multi-URL is enabled and SETTING_RESPONSE_RESOURCE_OWNER_URLS defined.
517
     *
518
     * @param $userId integer
519
     * @param $response array
520
     */
521
    private function updateUserUrls($userId, $response)
522
    {
523
        if (api_is_multiple_url_enabled()) {
524
            $key = (string) $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_URLS);
525
            if (!empty($key)) {
526
                $availableUrls = [];
527
                foreach (UrlManager::get_url_data() as $existingUrl) {
528
                    $urlId = $existingUrl['id'];
529
                    $availableUrls[strval($urlId)] = $urlId;
530
                    $availableUrls[$existingUrl['url']] = $urlId;
531
                }
532
                $allowedUrlIds = [];
533
                foreach ($this->getValuesByKey($response, $key) as $value) {
534
                    if (array_key_exists($value, $availableUrls)) {
535
                        $allowedUrlIds[] = $availableUrls[$value];
536
                    } else {
537
                        $newValue = ($value[-1] === '/') ? substr($value, 0, -1) : $value.'/';
538
                        if (array_key_exists($newValue, $availableUrls)) {
539
                            $allowedUrlIds[] = $availableUrls[$newValue];
540
                        }
541
                    }
542
                }
543
                $grantedUrlIds = [];
544
                foreach (UrlManager::get_access_url_from_user($userId) as $grantedUrl) {
545
                    $grantedUrlIds[] = $grantedUrl['access_url_id'];
546
                }
547
                foreach (array_diff($grantedUrlIds, $allowedUrlIds) as $extraUrlId) {
548
                    UrlManager::delete_url_rel_user($userId, $extraUrlId);
549
                }
550
                foreach (array_diff($allowedUrlIds, $grantedUrlIds) as $missingUrlId) {
551
                    UrlManager::add_user_to_url($userId, $missingUrlId);
552
                }
553
            }
554
        }
555
    }
556
557
    private function log(string $key, string $content)
558
    {
559
        if (self::DEBUG) {
560
            error_log("OAuth2 plugin: $key: $content");
561
        }
562
    }
563
}
564