OAuth   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 352
Duplicated Lines 0 %

Test Coverage

Coverage 47.37%

Importance

Changes 0
Metric Value
eloc 78
dl 0
loc 352
ccs 45
cts 95
cp 0.4737
rs 9.0399
c 0
b 0
f 0
wmc 42

24 Methods

Rating   Name   Duplication   Size   Complexity  
A setReturnUrl() 0 3 1
A createApiRequest() 0 3 1
A getSignatureMethod() 0 7 2
A setScope() 0 3 1
A getScope() 0 7 2
A getDefaultScope() 0 3 1
A __construct() 0 8 1
A beforeApiRequestSend() 0 8 3
A getAuthUrl() 0 3 1
A createSignatureMethod() 0 7 2
A getEndpoint() 0 3 1
A setEndpoint() 0 3 1
A withoutAutoRefreshAccessToken() 0 5 1
A restoreAccessToken() 0 10 4
A withAutoRefreshAccessToken() 0 5 1
A getAccessToken() 0 7 2
A defaultReturnUrl() 0 3 1
A setAuthUrl() 0 3 1
A createToken() 0 6 2
A saveAccessToken() 0 3 1
A getReturnUrl() 0 6 2
A setAccessToken() 0 7 3
A setSignatureMethod() 0 10 3
A api() 0 24 4

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\AuthClient;
6
7
use Exception;
8
use InvalidArgumentException;
9
use Psr\Http\Client\ClientInterface as PsrClientInterface;
10
use Psr\Http\Message\RequestFactoryInterface;
11
use Psr\Http\Message\RequestInterface;
12
use Psr\Http\Message\ServerRequestInterface;
13
use Psr\Http\Message\UriInterface;
14
use Yiisoft\Factory\Factory;
15
use Yiisoft\Json\Json;
16
use Yiisoft\Yii\AuthClient\Exception\InvalidResponseException;
17
use Yiisoft\Yii\AuthClient\Signature\HmacSha;
18
use Yiisoft\Yii\AuthClient\Signature\Signature;
19
use Yiisoft\Yii\AuthClient\StateStorage\StateStorageInterface;
20
21
use function is_array;
22
use function is_object;
23
24
/**
25
 * BaseOAuth is a base class for the OAuth clients.
26
 *
27
 * @link https://oauth.net/
28
 */
29
abstract class OAuth extends AuthClient
30
{
31
    /**
32
     * @var string API base URL.
33
     * This field will be used as {@see UriInterface::getPath()}} value of {@see httpClient}.
34
     * Note: changing this property will take no effect after {@see httpClient} is instantiated.
35
     */
36
    protected string $endpoint;
37
    /**
38
     * @var string authorize URL.
39
     */
40
    protected string $authUrl;
41
    /**
42
     * @var string string auth request scope.
43
     */
44
    protected ?string $scope = null;
45
    /**
46
     * @var bool whether to automatically perform 'refresh access token' request on expired access token.
47
     */
48
    protected bool $autoRefreshAccessToken = true;
49
50
    /**
51
     * @var string URL, which user will be redirected after authentication at the OAuth provider web site.
52
     * Note: this should be absolute URL (with http:// or https:// leading).
53
     * By default current URL will be used.
54
     */
55
    protected ?string $returnUrl = null;
56
    /**
57
     * @var array|OAuthToken access token instance or its array configuration.
58
     */
59
    protected $accessToken;
60
    /**
61
     * @var array|Signature signature method instance or its array configuration.
62
     */
63
    protected $signatureMethod = [];
64
    private Factory $factory;
65
66
    /**
67
     * BaseOAuth constructor.
68
     *
69
     * @param PsrClientInterface $httpClient
70
     * @param RequestFactoryInterface $requestFactory
71
     * @param StateStorageInterface $stateStorage
72
     * @param Factory $factory
73
     */
74 7
    public function __construct(
75
        PsrClientInterface $httpClient,
76
        RequestFactoryInterface $requestFactory,
77
        StateStorageInterface $stateStorage,
78
        Factory $factory
79
    ) {
80 7
        $this->factory = $factory;
81 7
        parent::__construct($httpClient, $requestFactory, $stateStorage);
82
    }
83
84
    public function getEndpoint(): string
85
    {
86
        return $this->endpoint;
87
    }
88
89
    public function setEndpoint(string $endpoint): void
90
    {
91
        $this->endpoint = $endpoint;
92
    }
93
94
    public function getAuthUrl(): string
95
    {
96
        return $this->authUrl;
97
    }
98
99 2
    public function setAuthUrl(string $authUrl): void
100
    {
101 2
        $this->authUrl = $authUrl;
102
    }
103
104
    /**
105
     * @param ServerRequestInterface $request
106
     *
107
     * @return string return URL.
108
     */
109 2
    public function getReturnUrl(ServerRequestInterface $request): string
110
    {
111 2
        if ($this->returnUrl === null) {
112 1
            $this->returnUrl = $this->defaultReturnUrl($request);
113
        }
114 2
        return $this->returnUrl;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->returnUrl could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
115
    }
116
117
    /**
118
     * @param string $returnUrl return URL
119
     */
120 1
    public function setReturnUrl(string $returnUrl): void
121
    {
122 1
        $this->returnUrl = $returnUrl;
123
    }
124
125
    /**
126
     * Composes default {@see returnUrl} value.
127
     *
128
     * @param ServerRequestInterface $request
129
     *
130
     * @return string return URL.
131
     */
132
    protected function defaultReturnUrl(ServerRequestInterface $request): string
133
    {
134
        return (string)$request->getUri();
135
    }
136
137
    /**
138
     * @return array|Signature signature method instance.
139
     */
140 3
    public function getSignatureMethod(): Signature
141
    {
142 3
        if (!is_object($this->signatureMethod)) {
143 2
            $this->signatureMethod = $this->createSignatureMethod($this->signatureMethod);
144
        }
145
146 3
        return $this->signatureMethod;
147
    }
148
149
    /**
150
     * Set signature method to be used.
151
     *
152
     * @param array|Signature $signatureMethod signature method instance or its array configuration.
153
     *
154
     * @throws InvalidArgumentException on wrong argument.
155
     */
156 1
    public function setSignatureMethod($signatureMethod): void
157
    {
158 1
        if (!is_object($signatureMethod) && !is_array($signatureMethod)) {
0 ignored issues
show
introduced by
The condition is_array($signatureMethod) is always true.
Loading history...
159
            throw new InvalidArgumentException(
160
                '"' . static::class . '::signatureMethod"'
161
                . ' should be instance of "\Yiisoft\Yii\AuthClient\Signature\BaseMethod" or its array configuration. "'
162
                . gettype($signatureMethod) . '" has been given.'
163
            );
164
        }
165 1
        $this->signatureMethod = $signatureMethod;
166
    }
167
168
    /**
169
     * Creates signature method instance from its configuration.
170
     *
171
     * @param array $signatureMethodConfig signature method configuration.
172
     *
173
     * @return object|Signature signature method instance.
174
     */
175 2
    protected function createSignatureMethod(array $signatureMethodConfig): Signature
176
    {
177 2
        if (!array_key_exists('class', $signatureMethodConfig)) {
178 2
            $signatureMethodConfig['class'] = HmacSha::class;
179 2
            $signatureMethodConfig['__construct()'] = ['sha1'];
180
        }
181 2
        return $this->factory->create($signatureMethodConfig);
182
    }
183
184
    public function withAutoRefreshAccessToken(): self
185
    {
186
        $new = clone $this;
187
        $new->autoRefreshAccessToken = true;
188
        return $new;
189
    }
190
191
    public function withoutAutoRefreshAccessToken(): self
192
    {
193
        $new = clone $this;
194
        $new->autoRefreshAccessToken = false;
195
        return $new;
196
    }
197
198
    /**
199
     * Performs request to the OAuth API returning response data.
200
     * You may use {@see createApiRequest()} method instead, gaining more control over request execution.
201
     *
202
     * @param string $apiSubUrl API sub URL, which will be append to {@see apiBaseUrl}, or absolute API URL.
203
     * @param string $method request method.
204
     * @param array|string $data request data or content.
205
     * @param array $headers additional request headers.
206
     *
207
     * @throws Exception
208
     *
209
     * @return array API response data.
210
     *
211
     * @see createApiRequest()
212
     */
213
    public function api($apiSubUrl, $method = 'GET', $data = [], $headers = []): array
214
    {
215
        $request = $this->createApiRequest($method, $apiSubUrl);
216
        $request = RequestUtil::addHeaders($request, $headers);
217
218
        if (!empty($data)) {
219
            if (is_array($data)) {
220
                $request = RequestUtil::addParams($request, $data);
221
            } else {
222
                $request->getBody()->write($data);
223
            }
224
        }
225
226
        $request = $this->beforeApiRequestSend($request);
227
        $response = $this->sendRequest($request);
228
229
        if ($response->getStatusCode() !== 200) {
230
            throw new InvalidResponseException(
231
                $response,
232
                'Request failed with code: ' . $response->getStatusCode() . ', message: ' . $response->getBody()
233
            );
234
        }
235
236
        return Json::decode($response->getBody()->getContents());
0 ignored issues
show
Bug Best Practice introduced by
The expression return Yiisoft\Json\Json...tBody()->getContents()) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
237
    }
238
239
    /**
240
     * Creates an HTTP request for the API call.
241
     * The created request will be automatically processed adding access token parameters and signature
242
     * before sending. You may use {@see createRequest()} to gain full control over request composition and execution.
243
     *
244
     * @param string $method
245
     * @param string $uri
246
     *
247
     * @return RequestInterface HTTP request instance.
248
     *
249
     * @see createRequest()
250
     */
251
    public function createApiRequest(string $method, string $uri): RequestInterface
252
    {
253
        return $this->createRequest($method, $this->endpoint . $uri);
254
    }
255
256
    public function beforeApiRequestSend(RequestInterface $request): RequestInterface
257
    {
258
        $accessToken = $this->getAccessToken();
259
        if (!is_object($accessToken) || !$accessToken->getIsValid()) {
260
            throw new Exception('Invalid access token.');
261
        }
262
263
        return $this->applyAccessTokenToRequest($request, $accessToken);
264
    }
265
266
    /**
267
     * @return OAuthToken auth token instance.
268
     */
269 3
    public function getAccessToken(): ?OAuthToken
270
    {
271 3
        if (!is_object($this->accessToken)) {
272 3
            $this->accessToken = $this->restoreAccessToken();
273
        }
274
275 3
        return $this->accessToken;
276
    }
277
278
    /**
279
     * Sets access token to be used.
280
     *
281
     * @param array|OAuthToken $token access token or its configuration.
282
     */
283 1
    public function setAccessToken($token): void
284
    {
285 1
        if (!is_object($token) && $token !== null) {
286
            $token = $this->createToken($token);
287
        }
288 1
        $this->accessToken = $token;
289 1
        $this->saveAccessToken($token);
290
    }
291
292
    /**
293
     * Restores access token.
294
     *
295
     * @return OAuthToken auth token.
296
     */
297 3
    protected function restoreAccessToken(): ?OAuthToken
298
    {
299 3
        $token = $this->getState('token');
300 3
        if (is_object($token)) {
301
            /* @var $token OAuthToken */
302
            if ($token->getIsExpired() && $this->autoRefreshAccessToken) {
303
                $token = $this->refreshAccessToken($token);
304
            }
305
        }
306 3
        return $token;
307
    }
308
309
    /**
310
     * Gets new auth token to replace expired one.
311
     *
312
     * @param OAuthToken $token expired auth token.
313
     *
314
     * @return OAuthToken new auth token.
315
     */
316
    abstract public function refreshAccessToken(OAuthToken $token): OAuthToken;
317
318
    /**
319
     * Applies access token to the HTTP request instance.
320
     *
321
     * @param RequestInterface $request HTTP request instance.
322
     * @param OAuthToken $accessToken access token instance.
323
     */
324
    abstract public function applyAccessTokenToRequest(
325
        RequestInterface $request,
326
        OAuthToken $accessToken
327
    ): RequestInterface;
328
329
    /**
330
     * Creates token from its configuration.
331
     *
332
     * @param array $tokenConfig token configuration.
333
     *
334
     * @throws \Yiisoft\Definitions\Exception\InvalidConfigException
335
     *
336
     * @return OAuthToken|object
337
     */
338 1
    protected function createToken(array $tokenConfig = [])
339
    {
340 1
        if (!array_key_exists('class', $tokenConfig)) {
341
            $tokenConfig['class'] = OAuthToken::class;
342
        }
343 1
        return $this->factory->create($tokenConfig);
344
    }
345
346
    /**
347
     * Saves token as persistent state.
348
     *
349
     * @param OAuthToken|null $token auth token to be saved.
350
     *
351
     * @return $this the object itself.
352
     */
353 1
    protected function saveAccessToken($token): self
354
    {
355 1
        return $this->setState('token', $token);
356
    }
357
358
    /**
359
     * @return string
360
     */
361 2
    public function getScope(): string
362
    {
363 2
        if ($this->scope === null) {
364 2
            return $this->getDefaultScope();
365
        }
366
367
        return $this->scope;
368
    }
369
370
    /**
371
     * @param string $scope
372
     */
373
    public function setScope(string $scope): void
374
    {
375
        $this->scope = $scope;
376
    }
377
378 2
    protected function getDefaultScope(): string
379
    {
380 2
        return '';
381
    }
382
}
383