LoginProvider   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 452
Duplicated Lines 0 %

Importance

Changes 11
Bugs 0 Features 0
Metric Value
eloc 98
c 11
b 0
f 0
dl 0
loc 452
rs 8.8798
wmc 44

29 Methods

Rating   Name   Duplication   Size   Complexity  
A __toString() 0 3 1
A getOauthAuthorizationOptions() 0 10 2
A getOauthScope() 0 10 2
A getIconUrl() 0 3 1
A getHandle() 0 5 1
A oauth1Callback() 0 16 1
A getClass() 0 5 1
A oauth2Connect() 0 20 2
A oauthCallback() 0 9 3
A getOauthProviderConfig() 0 3 1
A getScopeDocsUrl() 0 3 1
A getDefaultProfileFields() 0 3 1
A getRedirectUriWarning() 0 3 1
A getDefaultOauthAuthorizationOptions() 0 3 1
A oauth2Callback() 0 23 2
A getProfileFields() 0 10 2
A oauthVersion() 0 3 1
A mergeArrayValues() 0 17 5
A getLoginProviderConfig() 0 3 1
A getJavascriptOrigin() 0 3 1
A oauthConnect() 0 9 3
A getManagerUrl() 0 3 1
A oauth1Connect() 0 15 1
A isConfigured() 0 4 1
A getIsEnabled() 0 5 1
A getRedirectUri() 0 3 1
A getDefaultOauthScope() 0 3 1
A getUserFieldMapping() 0 10 2
A getProfile() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like LoginProvider 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 LoginProvider, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link      https://dukt.net/social/
4
 * @copyright Copyright (c) Dukt
5
 * @license   https://github.com/dukt/social/blob/v2/LICENSE.md
6
 */
7
8
namespace dukt\social\base;
9
10
use Craft;
11
use craft\web\Response;
12
use dukt\social\errors\LoginException;
13
use dukt\social\helpers\SocialHelper;
14
use dukt\social\models\Token;
15
use dukt\social\Plugin;
16
17
/**
18
 * LoginProvider is the base class for classes representing login providers in terms of objects.
19
 *
20
 * @author  Dukt <[email protected]>
21
 * @since   1.0
22
 */
23
abstract class LoginProvider implements LoginProviderInterface
24
{
25
    // Public Methods
26
    // =========================================================================
27
28
    /**
29
     * Use the login provider’s name as the string representation.
30
     *
31
     * @return string
32
     */
33
    public function __toString()
34
    {
35
        return $this->getName();
36
    }
37
38
    /**
39
     * Get the class name, stripping all the namespaces.
40
     *
41
     * For example, "dukt\social\loginproviders\Google" becomes "Google"
42
     *
43
     * @return string
44
     */
45
    public function getClass(): string
46
    {
47
        $nsClass = get_class($this);
48
49
        return substr($nsClass, strrpos($nsClass, "\\") + 1);
50
    }
51
52
    /**
53
     * Get the provider handle.
54
     *
55
     * @return string
56
     */
57
    public function getHandle(): string
58
    {
59
        $class = $this->getClass();
60
61
        return strtolower($class);
62
    }
63
64
    /**
65
     * Get the icon URL.
66
     *
67
     * @return mixed
68
     */
69
    public function getIconUrl()
70
    {
71
        return Craft::$app->assetManager->getPublishedUrl('@dukt/social/icons/' . $this->getHandle() . '.svg', true);
0 ignored issues
show
Unused Code introduced by
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

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

71
        return Craft::$app->assetManager->/** @scrutinizer ignore-call */ getPublishedUrl('@dukt/social/icons/' . $this->getHandle() . '.svg', true);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
72
    }
73
74
    /**
75
     * Checks if the login provider is configured.
76
     *
77
     * @return bool
78
     * @throws \yii\base\InvalidConfigException
79
     */
80
    public function isConfigured(): bool
81
    {
82
        $config = $this->getOauthProviderConfig();
83
        return !empty($config['options']['clientId']);
84
    }
85
86
    /**
87
     * Get API Manager URL.
88
     *
89
     * @return string|null
90
     */
91
    public function getManagerUrl()
92
    {
93
        return null;
94
    }
95
96
    /**
97
     * Get Scope Docs URL.
98
     *
99
     * @return string|null
100
     */
101
    public function getScopeDocsUrl()
102
    {
103
        return null;
104
    }
105
106
    /**
107
     * OAuth version.
108
     *
109
     * @return int
110
     */
111
    public function oauthVersion(): int
112
    {
113
        return 2;
114
    }
115
116
    /**
117
     * OAuth connect.
118
     *
119
     * @return Response
120
     * @throws LoginException
121
     * @throws \craft\errors\MissingComponentException
122
     * @throws \yii\base\InvalidConfigException
123
     */
124
    public function oauthConnect(): Response
125
    {
126
        if ($this->oauthVersion() == 1) {
127
            return $this->oauth1Connect();
128
        } elseif ($this->oauthVersion() == 2) {
129
            return $this->oauth2Connect();
130
        }
131
132
        throw new LoginException('OAuth version not supported');
133
    }
134
135
    /**
136
     * OAuth callback.
137
     *
138
     * @return array
139
     * @throws LoginException
140
     * @throws \craft\errors\MissingComponentException
141
     */
142
    public function oauthCallback(): array
143
    {
144
        if ($this->oauthVersion() == 1) {
145
            return $this->oauth1Callback();
146
        } elseif ($this->oauthVersion() == 2) {
147
            return $this->oauth2Callback();
148
        }
149
150
        throw new LoginException('OAuth version not supported');
151
    }
152
153
    /**
154
     * Returns the `scope` from login provider class by default, or the `scope` overridden by the config.
155
     *
156
     * @return array
157
     * @throws \yii\base\InvalidConfigException
158
     */
159
    public function getOauthScope(): array
160
    {
161
        $scope = $this->getDefaultOauthScope();
162
        $oauthProviderConfig = $this->getOauthProviderConfig();
163
164
        if (isset($oauthProviderConfig['scope'])) {
165
            $scope = $this->mergeArrayValues($scope, $oauthProviderConfig['scope']);
166
        }
167
168
        return $scope;
169
    }
170
171
    /**
172
     * Returns the OAuth authorization options for this provider.
173
     *
174
     * @return array|null
175
     * @throws \yii\base\InvalidConfigException
176
     */
177
    public function getOauthAuthorizationOptions()
178
    {
179
        $authorizationOptions = $this->getDefaultOauthAuthorizationOptions();
180
        $config = $this->getOauthProviderConfig();
181
182
        if (isset($config['authorizationOptions'])) {
183
            $authorizationOptions = array_merge($authorizationOptions, $config['authorizationOptions']);
184
        }
185
186
        return $authorizationOptions;
187
    }
188
189
    /**
190
     * Returns the `enabled` setting from login provider class by default, or `enabled` overridden by the config.
191
     *
192
     * @return bool
193
     */
194
    public function getIsEnabled(): bool
195
    {
196
        // get plugin settings
197
        $enabledLoginProviders = Plugin::getInstance()->getSettings()->enabledLoginProviders;
198
        return in_array($this->getHandle(), $enabledLoginProviders, true);
199
    }
200
201
    /**
202
     * Returns the Javascript origin URL.
203
     *
204
     * @return string|null
205
     */
206
    public function getJavascriptOrigin()
207
    {
208
        return null;
209
    }
210
211
    /**
212
     * Returns the URI users are redirected to after they have connected.
213
     *
214
     * @return string
215
     */
216
    public function getRedirectUri(): string
217
    {
218
        return SocialHelper::siteActionUrl('social/login-accounts/callback');
219
    }
220
221
    /**
222
     * Returns a warning for the OAuth redirect URI.
223
     *
224
     * @return string|null
225
     */
226
    public function getRedirectUriWarning()
227
    {
228
        return null;
229
    }
230
231
    /**
232
     * Get profile fields.
233
     *
234
     * @return array
235
     */
236
    public function getProfileFields(): array
237
    {
238
        $profileFields = $this->getDefaultProfileFields();
239
        $loginProviderConfig = Plugin::getInstance()->getLoginProviderConfig($this->getHandle());
240
241
        if (isset($loginProviderConfig['profileFields'])) {
242
            $profileFields = $this->mergeArrayValues($profileFields, $loginProviderConfig['profileFields']);
243
        }
244
245
        return $profileFields;
246
    }
247
248
    /**
249
     * Get user field mapping.
250
     *
251
     * @return array
252
     */
253
    public function getUserFieldMapping(): array
254
    {
255
        $userFieldMapping = $this->getDefaultUserFieldMapping();
256
        $loginProviderConfig = Plugin::getInstance()->getLoginProviderConfig($this->getHandle());
257
258
        if (isset($loginProviderConfig['userFieldMapping'])) {
259
            $userFieldMapping = array_merge($userFieldMapping, $loginProviderConfig['userFieldMapping']);
260
        }
261
262
        return $userFieldMapping;
263
    }
264
265
    /**
266
     * Returns a profile from an OAuth token.
267
     *
268
     * @param Token $token
269
     *
270
     * @return array|null
271
     */
272
    public function getProfile(Token $token)
273
    {
274
        $profile = $this->getOauthProvider()->getResourceOwner($token->token);
275
276
        if (!$profile) {
277
            return null;
278
        }
279
280
        return $profile;
281
    }
282
283
    // Protected Methods
284
    // =========================================================================
285
286
    /**
287
     * Get the default authorization options.
288
     *
289
     * @return array
290
     */
291
    protected function getDefaultOauthAuthorizationOptions(): array
292
    {
293
        return [];
294
    }
295
296
    /**
297
     * Get the default scope.
298
     *
299
     * @return array
300
     */
301
    protected function getDefaultOauthScope(): array
302
    {
303
        return [];
304
    }
305
306
    /**
307
     * Get default profile fields.
308
     *
309
     * @return array
310
     */
311
    protected function getDefaultProfileFields(): array
312
    {
313
        return [];
314
    }
315
316
    /**
317
     * Get OAuth provider config.
318
     *
319
     * @return array
320
     * @throws \yii\base\InvalidConfigException
321
     */
322
    protected function getOauthProviderConfig(): array
323
    {
324
        return Plugin::getInstance()->getOauthProviderConfig($this->getHandle());
325
    }
326
327
    /**
328
     * Get login provider config.
329
     *
330
     * @return array
331
     */
332
    protected function getLoginProviderConfig(): array
333
    {
334
        return Plugin::getInstance()->getLoginProviderConfig($this->getHandle());
335
    }
336
337
    // Private Methods
338
    // =========================================================================
339
340
    /**
341
     * OAuth 1 connect.
342
     *
343
     * @return Response
344
     * @throws \craft\errors\MissingComponentException
345
     */
346
    private function oauth1Connect(): Response
347
    {
348
        // OAuth provider
349
        $provider = $this->getOauthProvider();
350
351
        // Obtain temporary credentials
352
        $temporaryCredentials = $provider->getTemporaryCredentials();
353
354
        // Store credentials in the session
355
        Craft::$app->getSession()->set('oauth.temporaryCredentials', $temporaryCredentials);
356
357
        // Redirect to login screen
358
        $authorizationUrl = $provider->getAuthorizationUrl($temporaryCredentials);
359
360
        return Craft::$app->getResponse()->redirect($authorizationUrl);
0 ignored issues
show
Bug Best Practice introduced by
The expression return Craft::app->getRe...rect($authorizationUrl) could return the type yii\console\Response which is incompatible with the type-hinted return craft\web\Response. Consider adding an additional type-check to rule them out.
Loading history...
361
    }
362
363
    /**
364
     * OAuth 2 connect.
365
     *
366
     * @return Response
367
     * @throws \craft\errors\MissingComponentException
368
     * @throws \yii\base\InvalidConfigException
369
     */
370
    private function oauth2Connect(): Response
371
    {
372
        $provider = $this->getOauthProvider();
373
        $scope = $this->getOauthScope();
374
        $options = $this->getOauthAuthorizationOptions();
375
376
        if (!is_array($options)) {
0 ignored issues
show
introduced by
The condition is_array($options) is always true.
Loading history...
377
            $options = [];
378
        }
379
380
        $options['scope'] = $scope;
381
382
        $authorizationUrl = $provider->getAuthorizationUrl($options);
383
384
385
        $state = $provider->getState();
386
387
        Craft::$app->getSession()->set('social.oauth2State', $state);
388
389
        return Craft::$app->getResponse()->redirect($authorizationUrl);
0 ignored issues
show
Bug Best Practice introduced by
The expression return Craft::app->getRe...rect($authorizationUrl) could return the type yii\console\Response which is incompatible with the type-hinted return craft\web\Response. Consider adding an additional type-check to rule them out.
Loading history...
390
    }
391
392
    /**
393
     * OAuth 1 callback.
394
     *
395
     * @return array
396
     * @throws \craft\errors\MissingComponentException
397
     */
398
    private function oauth1Callback(): array
399
    {
400
        $provider = $this->getOauthProvider();
401
402
        $oauthToken = Craft::$app->getRequest()->getParam('oauth_token');
403
        $oauthVerifier = Craft::$app->getRequest()->getParam('oauth_verifier');
404
405
        // Retrieve the temporary credentials we saved before.
406
        $temporaryCredentials = Craft::$app->getSession()->get('oauth.temporaryCredentials');
407
408
        // Obtain token credentials from the server.
409
        $token = $provider->getTokenCredentials($temporaryCredentials, $oauthToken, $oauthVerifier);
410
411
        return [
412
            'success' => true,
413
            'token' => $token
414
        ];
415
    }
416
417
    /**
418
     * OAuth 2 callback.
419
     *
420
     * @return array
421
     * @throws LoginException
422
     * @throws \craft\errors\MissingComponentException
423
     */
424
    private function oauth2Callback(): array
425
    {
426
        $provider = $this->getOauthProvider();
427
428
        // Check given state against previously stored one to mitigate CSRF attack
429
        $sessionState = Craft::$app->getSession()->get('social.oauth2State');
430
        $getState = Craft::$app->getRequest()->getParam('state');
431
432
        if ($sessionState !== $getState) {
433
            throw new LoginException('Invalid OAuth state.');
434
        }
435
436
        // Get the OAuth code
437
        $code = Craft::$app->getRequest()->getParam('code');
438
439
        // Try to get an access token (using the authorization code grant)
440
        $token = $provider->getAccessToken('authorization_code', [
441
            'code' => $code
442
        ]);
443
444
        return [
445
            'success' => true,
446
            'token' => $token
447
        ];
448
    }
449
450
    /**
451
     * Merge scope.
452
     *
453
     * @param array $array
454
     * @param array $array2
455
     *
456
     * @return array
457
     */
458
    private function mergeArrayValues(array $array, array $array2): array
459
    {
460
        foreach ($array2 as $value2) {
461
            $addValue = true;
462
463
            foreach ($array as $value) {
464
                if ($value === $value2) {
465
                    $addValue = false;
466
                }
467
            }
468
469
            if ($addValue) {
470
                $array[] = $value2;
471
            }
472
        }
473
474
        return $array;
475
    }
476
}
477