Passed
Push — master ( 13425d...f357db )
by Alexander
02:26
created

OAuth::api()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 6
nop 4
dl 0
loc 24
ccs 0
cts 14
cp 0
crap 20
rs 9.7998
c 0
b 0
f 0
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\FactoryInterface;
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 http://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 FactoryInterface $factory;
65
66
    /**
67
     * BaseOAuth constructor.
68
     *
69
     * @param PsrClientInterface $httpClient
70
     * @param RequestFactoryInterface $requestFactory
71
     * @param StateStorageInterface $stateStorage
72
     * @param FactoryInterface $factory
73
     */
74 6
    public function __construct(
75
        PsrClientInterface $httpClient,
76
        RequestFactoryInterface $requestFactory,
77
        StateStorageInterface $stateStorage,
78
        FactoryInterface $factory
79
    ) {
80 6
        $this->factory = $factory;
81 6
        parent::__construct($httpClient, $requestFactory, $stateStorage);
82 6
    }
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 1
    public function setAuthUrl(string $authUrl): void
100
    {
101 1
        $this->authUrl = $authUrl;
102 1
    }
103
104
    /**
105
     * @param ServerRequestInterface $request
106
     *
107
     * @return string return URL.
108
     */
109 1
    public function getReturnUrl(ServerRequestInterface $request): string
110
    {
111 1
        if ($this->returnUrl === null) {
112
            $this->returnUrl = $this->defaultReturnUrl($request);
113
        }
114 1
        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 1
    }
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 2
    public function getSignatureMethod(): Signature
141
    {
142 2
        if (!is_object($this->signatureMethod)) {
143 1
            $this->signatureMethod = $this->createSignatureMethod($this->signatureMethod);
144
        }
145
146 2
        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 1
    }
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 1
    protected function createSignatureMethod(array $signatureMethodConfig): Signature
176
    {
177 1
        if (!array_key_exists('__class', $signatureMethodConfig)) {
178 1
            $signatureMethodConfig['__class'] = HmacSha::class;
179 1
            $signatureMethodConfig['__construct()'] = ['sha1'];
180
        }
181 1
        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 2
    public function getAccessToken(): ?OAuthToken
270
    {
271 2
        if (!is_object($this->accessToken)) {
272 2
            $this->accessToken = $this->restoreAccessToken();
273
        }
274
275 2
        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
    public function setAccessToken($token): void
284
    {
285
        if (!is_object($token) && $token !== null) {
286
            $token = $this->createToken($token);
287
        }
288
        $this->accessToken = $token;
289
        $this->saveAccessToken($token);
290
    }
291
292
    /**
293
     * Restores access token.
294
     *
295
     * @return OAuthToken auth token.
296
     */
297 2
    protected function restoreAccessToken(): ?OAuthToken
298
    {
299 2
        $token = $this->getState('token');
300 2
        if (is_object($token)) {
301
            /* @var $token OAuthToken */
302
            if ($token->getIsExpired() && $this->autoRefreshAccessToken) {
303
                $token = $this->refreshAccessToken($token);
304
            }
305
        }
306 2
        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\Factory\Exceptions\InvalidConfigException
335
     *
336
     * @return OAuthToken|object
337
     */
338
    protected function createToken(array $tokenConfig = [])
339
    {
340
        if (!array_key_exists('__class', $tokenConfig)) {
341
            $tokenConfig['__class'] = OAuthToken::class;
342
        }
343
        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
    protected function saveAccessToken($token): self
354
    {
355
        return $this->setState('token', $token);
356
    }
357
358
    /**
359
     * @return string
360
     */
361 1
    public function getScope(): string
362
    {
363 1
        if ($this->scope === null) {
364 1
            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 1
    protected function getDefaultScope(): string
379
    {
380 1
        return '';
381
    }
382
}
383