Passed
Pull Request — 1.11.x (#4739)
by Angel Fernando Quiroz
10:12
created

OAuth2::getUserInfo()   C

Complexity

Conditions 14
Paths 16

Size

Total Lines 112
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 79
c 1
b 0
f 0
dl 0
loc 112
rs 5.4715
cc 14
nc 16
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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