Passed
Push — 1.11.x ( 4c54a9...7de0a3 )
by Angel Fernando Quiroz
09:36
created

OAuth2   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 211
c 2
b 0
f 0
dl 0
loc 378
rs 8.72
wmc 46

10 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 5 2
A __construct() 0 47 1
A getSignInURL() 0 3 1
B getProvider() 0 45 8
B getValuesByKey() 0 25 7
A install() 0 7 1
C getUserInfo() 0 71 11
B updateUserUrls() 0 31 11
A getLogoutUrl() 0 3 1
A updateUser() 0 41 3

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 League\OAuth2\Client\Provider\Exception\IdentityProviderException;
5
use League\OAuth2\Client\Provider\GenericProvider;
6
use League\OAuth2\Client\Token\AccessToken;
7
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
8
9
/**
10
 * OAuth2 plugin class.
11
 *
12
 * @author Sébastien Ducoulombier <[email protected]>
13
 * inspired by AzureActiveDirectory plugin class from Angel Fernando Quiroz Campos <[email protected]>
14
 *
15
 * @package chamilo.plugin.oauth2
16
 */
17
class OAuth2 extends Plugin
18
{
19
    use ArrayAccessorTrait;
20
21
    const SETTING_ENABLE = 'enable';
22
23
    const SETTING_CLIENT_ID = 'client_id';
24
    const SETTING_CLIENT_SECRET = 'client_secret';
25
26
    const SETTING_AUTHORIZE_URL = 'authorize_url';
27
    const SETTING_SCOPES = 'scopes';
28
    const SETTING_SCOPE_SEPARATOR = 'scope_separator';
29
30
    const SETTING_ACCESS_TOKEN_URL = 'access_token_url';
31
    const SETTING_ACCESS_TOKEN_METHOD = 'access_token_method';
32
    // const SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID = 'access_token_resource_owner_id';
33
34
    const SETTING_RESOURCE_OWNER_DETAILS_URL = 'resource_owner_details_url';
35
36
    const SETTING_RESPONSE_ERROR = 'response_error';
37
    const SETTING_RESPONSE_CODE = 'response_code';
38
    const SETTING_RESPONSE_RESOURCE_OWNER_ID = 'response_resource_owner_id';
39
40
    const SETTING_UPDATE_USER_INFO = 'update_user_info';
41
    const SETTING_CREATE_NEW_USERS = 'create_new_users';
42
    const SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME = 'response_resource_owner_firstname';
43
    const SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME = 'response_resource_owner_lastname';
44
    const SETTING_RESPONSE_RESOURCE_OWNER_STATUS = 'response_resource_owner_status';
45
    const SETTING_RESPONSE_RESOURCE_OWNER_EMAIL = 'response_resource_owner_email';
46
    const SETTING_RESPONSE_RESOURCE_OWNER_USERNAME = 'response_resource_owner_username';
47
48
    const SETTING_RESPONSE_RESOURCE_OWNER_URLS = 'response_resource_owner_urls';
49
50
    const SETTING_LOGOUT_URL = 'logout_url';
51
52
    const SETTING_BLOCK_NAME = 'block_name';
53
54
    const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable';
55
    const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name';
56
57
    const EXTRA_FIELD_OAUTH2_ID = 'oauth2_id';
58
59
    protected function __construct()
60
    {
61
        parent::__construct(
62
            '0.1',
63
            'Sébastien Ducoulombier',
64
            [
65
                self::SETTING_ENABLE => 'boolean',
66
67
                self::SETTING_CLIENT_ID => 'text',
68
                self::SETTING_CLIENT_SECRET => 'text',
69
70
                self::SETTING_AUTHORIZE_URL => 'text',
71
                self::SETTING_SCOPES => 'text',
72
                self::SETTING_SCOPE_SEPARATOR => 'text',
73
74
                self::SETTING_ACCESS_TOKEN_URL => 'text',
75
                self::SETTING_ACCESS_TOKEN_METHOD => [
76
                    'type' => 'select',
77
                    'options' => [
78
                        GenericProvider::METHOD_POST => 'POST',
79
                        GenericProvider::METHOD_GET => 'GET',
80
                    ],
81
                ],
82
                // self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID => 'text',
83
84
                self::SETTING_RESOURCE_OWNER_DETAILS_URL => 'text',
85
86
                self::SETTING_RESPONSE_ERROR => 'text',
87
                self::SETTING_RESPONSE_CODE => 'text',
88
                self::SETTING_RESPONSE_RESOURCE_OWNER_ID => 'text',
89
90
                self::SETTING_UPDATE_USER_INFO => 'boolean',
91
                self::SETTING_CREATE_NEW_USERS => 'boolean',
92
                self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME => 'text',
93
                self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME => 'text',
94
                self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS => 'text',
95
                self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL => 'text',
96
                self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME => 'text',
97
98
                self::SETTING_RESPONSE_RESOURCE_OWNER_URLS => 'text',
99
100
                self::SETTING_LOGOUT_URL => 'text',
101
102
                self::SETTING_BLOCK_NAME => 'text',
103
104
                self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean',
105
                self::SETTING_MANAGEMENT_LOGIN_NAME => 'text',
106
            ]
107
        );
108
    }
109
110
    /**
111
     * Instance the plugin.
112
     *
113
     * @staticvar null $result
114
     *
115
     * @return $this
116
     */
117
    public static function create()
118
    {
119
        static $result = null;
120
121
        return $result ? $result : $result = new self();
122
    }
123
124
    public function getProvider(): GenericProvider
125
    {
126
        $options = [
127
            'clientId' => $this->get(self::SETTING_CLIENT_ID),
128
            'clientSecret' => $this->get(self::SETTING_CLIENT_SECRET),
129
            'redirectUri' => api_get_path(WEB_PLUGIN_PATH).'oauth2/src/callback.php',
130
            'urlAuthorize' => $this->get(self::SETTING_AUTHORIZE_URL),
131
            'urlResourceOwnerDetails' => $this->get(self::SETTING_RESOURCE_OWNER_DETAILS_URL),
132
        ];
133
134
        if ('' === $scopeSeparator = (string) $this->get(self::SETTING_SCOPE_SEPARATOR)) {
135
            $scopeSeparator = ' ';
136
        }
137
138
        $options['scopeSeparator'] = $scopeSeparator;
139
140
        if ('' !== $scopes = (string) $this->get(self::SETTING_SCOPES)) {
141
            $options['scopes'] = explode($scopeSeparator, $scopes);
142
        }
143
144
        if ('' !== $urlAccessToken = (string) $this->get(self::SETTING_ACCESS_TOKEN_URL)) {
145
            $options['urlAccessToken'] = $urlAccessToken;
146
        }
147
148
        if ('' !== $accessTokenMethod = (string) $this->get(self::SETTING_ACCESS_TOKEN_METHOD)) {
149
            $options['accessTokenMethod'] = $accessTokenMethod;
150
        }
151
152
//        if ('' !== $accessTokenResourceOwnerId = (string) $this->get(self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID)) {
153
//            $options['accessTokenResourceOwnerId'] = $accessTokenResourceOwnerId;
154
//        }
155
156
        if ('' !== $responseError = (string) $this->get(self::SETTING_RESPONSE_ERROR)) {
157
            $options['responseError'] = $responseError;
158
        }
159
160
        if ('' !== $responseCode = (string) $this->get(self::SETTING_RESPONSE_CODE)) {
161
            $options['responseCode'] = $responseCode;
162
        }
163
164
        if ('' !== $responseResourceOwnerId = (string) $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)) {
165
            $options['responseResourceOwnerId'] = $responseResourceOwnerId;
166
        }
167
168
        return new GenericProvider($options);
169
    }
170
171
    /**
172
     * @throws IdentityProviderException
173
     *
174
     * @return array user information, as returned by api_get_user_info(userId)
175
     *
176
     * @var AccessToken
177
     * @var GenericProvider
178
     */
179
    public function getUserInfo($provider, $accessToken)
180
    {
181
        $url = $provider->getResourceOwnerDetailsUrl($accessToken);
182
        $request = $provider->getAuthenticatedRequest($provider::METHOD_GET, $url, $accessToken);
183
        $response = $provider->getParsedResponse($request);
184
        if (false === is_array($response)) {
185
            throw new UnexpectedValueException($this->get_lang('InvalidJsonReceivedFromProvider'));
186
        }
187
        $resourceOwnerId = $this->getValueByKey(
188
            $response,
189
            $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)
190
        );
191
        if (empty($resourceOwnerId)) {
192
            throw new RuntimeException($this->get_lang('WrongResponseResourceOwnerId'));
193
        }
194
        $extraFieldValue = new ExtraFieldValue('user');
195
        $result = $extraFieldValue->get_item_id_from_field_variable_and_field_value(
196
            self::EXTRA_FIELD_OAUTH2_ID,
197
            $resourceOwnerId
198
        );
199
        if (false === $result) {
200
            // authenticated user not found in internal database
201
            if ('true' !== $this->get(self::SETTING_CREATE_NEW_USERS)) {
202
                throw new RuntimeException($this->get_lang('NoUserHasThisOauthCode'));
203
            }
204
            require_once __DIR__.'/../../../main/auth/external_login/functions.inc.php';
205
            $userId = external_add_user(
206
                [
207
                    'firstname' => $this->getValueByKey($response, $this->get(
208
                        self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME
209
                    ), $this->get_lang('DefaultFirstname')),
210
                    'lastname' => $this->getValueByKey($response, $this->get(
211
                        self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME
212
                    ), $this->get_lang('DefaultLastname')),
213
                    'status' => $this->getValueByKey($response, $this->get(
214
                        self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS
215
                    ), STUDENT),
216
                    'email' => $this->getValueByKey($response, $this->get(
217
                        self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL
218
                    ), 'oauth2user_'.$resourceOwnerId.'@'.(gethostname() or 'localhost')),
219
                    'username' => $this->getValueByKey($response, $this->get(
220
                        self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME
221
                    ), 'oauth2user_'.$resourceOwnerId),
222
                    'auth_source' => 'oauth2',
223
                ]
224
            );
225
            if (false === $userId) {
226
                throw new RuntimeException($this->get_lang('FailedUserCreation'));
227
            }
228
            $this->updateUser($userId, $response);
229
            // Not checking function update_extra_field_value return value because not reliable
230
            UserManager::update_extra_field_value($userId, self::EXTRA_FIELD_OAUTH2_ID, $resourceOwnerId);
231
            $this->updateUserUrls($userId, $response);
232
        } else {
233
            // authenticated user found in internal database
234
            if (is_array($result) and array_key_exists('item_id', $result)) {
235
                $userId = $result['item_id'];
236
            } else {
237
                $userId = $result;
238
            }
239
            if ('true' === $this->get(self::SETTING_UPDATE_USER_INFO)) {
240
                $this->updateUser($userId, $response);
241
                $this->updateUserUrls($userId, $response);
242
            }
243
        }
244
        $userInfo = api_get_user_info($userId);
245
        if (empty($userInfo)) {
246
            throw new LogicException($this->get_lang('InternalErrorCannotGetUserInfo'));
247
        }
248
249
        return $userInfo;
250
    }
251
252
    public function getSignInURL()
253
    {
254
        return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php';
255
    }
256
257
    public function getLogoutUrl()
258
    {
259
        return $this->get(self::SETTING_LOGOUT_URL);
260
    }
261
262
    /**
263
     * Create extra fields for user when installing.
264
     */
265
    public function install()
266
    {
267
        UserManager::create_extra_field(
268
            self::EXTRA_FIELD_OAUTH2_ID,
269
            ExtraField::FIELD_TYPE_TEXT,
270
            $this->get_lang('OAuth2Id'),
271
            ''
272
        );
273
    }
274
275
    /**
276
     * Extends ArrayAccessorTrait::getValueByKey to return a list of values
277
     * $key can contain wild card character *
278
     * It will be replaced by 0, 1, 2 and so on as long as the resulting key exists in $data
279
     * This is a recursive function, allowing for more than one occurrence of the wild card character.
280
     *
281
     * @param string $key
282
     * @param array  $default
283
     *
284
     * @return array
285
     */
286
    private function getValuesByKey(array $data, $key, $default = [])
287
    {
288
        if (!is_string($key) || empty($key) || !count($data)) {
289
            return $default;
290
        }
291
        $pos = strpos($key, '*');
292
        if ($pos === false) {
293
            $value = $this->getValueByKey($data, $key, null);
294
295
            return is_null($value) ? [] : [$value];
296
        }
297
        $values = [];
298
        $beginning = substr($key, 0, $pos);
299
        $remaining = substr($key, $pos + 1);
300
        $index = 0;
301
        do {
302
            $newValues = $this->getValuesByKey(
303
                $data,
304
                $beginning.$index.$remaining
305
            );
306
            $values = array_merge($values, $newValues);
307
            $index++;
308
        } while ($newValues);
309
310
        return $values;
311
    }
312
313
    private function updateUser($userId, $response)
314
    {
315
        /**
316
         * @var $user Chamilo\UserBundle\Entity\User
317
         */
318
        $user = UserManager::getRepository()->find($userId);
319
        $user->setFirstname(
320
            $this->getValueByKey($response, $this->get(
321
                self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME
322
            ), $user->getFirstname())
323
        );
324
        $user->setLastname(
325
            $this->getValueByKey($response, $this->get(
326
                self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME
327
            ), $user->getLastname())
328
        );
329
        $user->setUserName(
330
            $this->getValueByKey($response, $this->get(
331
                self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME
332
            ), $user->getUsername())
333
        );
334
        $user->setEmail(
335
            $this->getValueByKey($response, $this->get(
336
                self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL
337
            ), $user->getEmail())
338
        );
339
        $user->setStatus(
340
            $this->getValueByKey($response, $this->get(
341
                self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS
342
            ), $user->getStatus())
343
        );
344
        $user->setAuthSource('oauth2');
345
        $configFilePath = __DIR__.'/../config.php';
346
        if (file_exists($configFilePath)) {
347
            require_once $configFilePath;
348
            $functionName = 'oauth2UpdateUserFromResourceOwnerDetails';
349
            if (function_exists($functionName)) {
350
                $functionName($response, $user);
351
            }
352
        }
353
        UserManager::getManager()->updateUser($user);
354
    }
355
356
    /**
357
     * Updates the Access URLs associated to a user
358
     * according to the OAuth2 server response resource owner
359
     * if multi-URL is enabled and SETTING_RESPONSE_RESOURCE_OWNER_URLS defined.
360
     *
361
     * @param $userId integer
362
     * @param $response array
363
     */
364
    private function updateUserUrls($userId, $response)
365
    {
366
        if (api_is_multiple_url_enabled()) {
367
            $key = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_URLS);
368
            if (!empty($key)) {
369
                $availableUrls = [];
370
                foreach (URLManager::get_url_data() as $existingUrl) {
371
                    $urlId = $existingUrl['id'];
372
                    $availableUrls[strval($urlId)] = $urlId;
373
                    $availableUrls[$existingUrl['url']] = $urlId;
374
                }
375
                $allowedUrlIds = [];
376
                foreach ($this->getValuesByKey($response, $key) as $value) {
377
                    if (array_key_exists($value, $availableUrls)) {
378
                        $allowedUrlIds[] = $availableUrls[$value];
379
                    } else {
380
                        $newValue = ($value[-1] === '/') ? substr($value, 0, -1) : $value.'/';
381
                        if (array_key_exists($newValue, $availableUrls)) {
382
                            $allowedUrlIds[] = $availableUrls[$newValue];
383
                        }
384
                    }
385
                }
386
                $grantedUrlIds = [];
387
                foreach (URLManager::get_access_url_from_user($userId) as $grantedUrl) {
388
                    $grantedUrlIds[] = $grantedUrl['access_url_id'];
389
                }
390
                foreach (array_diff($grantedUrlIds, $allowedUrlIds) as $extraUrlId) {
391
                    URLManager::delete_url_rel_user($userId, $extraUrlId);
392
                }
393
                foreach (array_diff($allowedUrlIds, $grantedUrlIds) as $missingUrlId) {
394
                    URLManager::add_user_to_url($userId, $missingUrlId);
395
                }
396
            }
397
        }
398
    }
399
}
400