Completed
Pull Request — master (#144)
by
unknown
01:26
created

AbstractProvider::setAutoSaveState()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the overtrue/socialite.
5
 *
6
 * (c) overtrue <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Overtrue\Socialite\Providers;
13
14
use GuzzleHttp\Client;
15
use GuzzleHttp\ClientInterface;
16
use Overtrue\Socialite\AccessToken;
17
use Overtrue\Socialite\AccessTokenInterface;
18
use Overtrue\Socialite\AuthorizeFailedException;
19
use Overtrue\Socialite\InvalidStateException;
20
use Overtrue\Socialite\ProviderInterface;
21
use Symfony\Component\HttpFoundation\RedirectResponse;
22
use Symfony\Component\HttpFoundation\Request;
23
24
/**
25
 * Class AbstractProvider.
26
 */
27
abstract class AbstractProvider implements ProviderInterface
28
{
29
    /**
30
     * Provider name.
31
     *
32
     * @var string
33
     */
34
    protected $name;
35
36
    /**
37
     * The HTTP request instance.
38
     *
39
     * @var \Symfony\Component\HttpFoundation\Request
40
     */
41
    protected $request;
42
43
    /**
44
     * The client ID.
45
     *
46
     * @var string
47
     */
48
    protected $clientId;
49
50
    /**
51
     * The client secret.
52
     *
53
     * @var string
54
     */
55
    protected $clientSecret;
56
57
    /**
58
     * @var \Overtrue\Socialite\AccessTokenInterface
59
     */
60
    protected $accessToken;
61
62
    /**
63
     * The redirect URL.
64
     *
65
     * @var string
66
     */
67
    protected $redirectUrl;
68
69
    /**
70
     * The custom parameters to be sent with the request.
71
     *
72
     * @var array
73
     */
74
    protected $parameters = [];
75
76
    /**
77
     * The scopes being requested.
78
     *
79
     * @var array
80
     */
81
    protected $scopes = [];
82
83
    /**
84
     * The separating character for the requested scopes.
85
     *
86
     * @var string
87
     */
88
    protected $scopeSeparator = ',';
89
90
    /**
91
     * The type of the encoding in the query.
92
     *
93
     * @var int Can be either PHP_QUERY_RFC3986 or PHP_QUERY_RFC1738
94
     */
95
    protected $encodingType = PHP_QUERY_RFC1738;
96
97
    /**
98
     * Indicates if the session state should be utilized.
99
     *
100
     * @var bool
101
     */
102
    protected $stateless = false;
103
104
    /**
105
     * Indicates if the state should be save to session.
106
     *
107
     * @var bool
108
     */
109
    protected $autoSaveState = true;
110
    /**
111
     * The options for guzzle\client.
112
     *
113
     * @var array
114
     */
115
    protected static $guzzleOptions = ['http_errors' => false];
116
117
    /**
118
     * Create a new provider instance.
119
     *
120
     * @param \Symfony\Component\HttpFoundation\Request $request
121
     * @param string                                    $clientId
122
     * @param string                                    $clientSecret
123
     * @param string|null                               $redirectUrl
124
     */
125
    public function __construct(Request $request, $clientId, $clientSecret, $redirectUrl = null)
0 ignored issues
show
Bug introduced by
You have injected the Request via parameter $request. This is generally not recommended as there might be multiple instances during a request cycle (f.e. when using sub-requests). Instead, it is recommended to inject the RequestStack and retrieve the current request each time you need it via getCurrentRequest().
Loading history...
126
    {
127
        $this->request = $request;
128
        $this->clientId = $clientId;
129
        $this->clientSecret = $clientSecret;
130
        $this->redirectUrl = $redirectUrl;
131
    }
132
133
    /**
134
     * Get the authentication URL for the provider.
135
     *
136
     * @param string $state
137
     *
138
     * @return string
139
     */
140
    abstract public function getAuthUrl($state);
141
142
    /**
143
     * Get the token URL for the provider.
144
     *
145
     * @return string
146
     */
147
    abstract protected function getTokenUrl();
148
149
    /**
150
     * Get the raw user for the given access token.
151
     *
152
     * @param \Overtrue\Socialite\AccessTokenInterface $token
153
     *
154
     * @return array
155
     */
156
    abstract protected function getUserByToken(AccessTokenInterface $token);
157
158
    /**
159
     * Map the raw user array to a Socialite User instance.
160
     *
161
     * @param array $user
162
     *
163
     * @return \Overtrue\Socialite\User
164
     */
165
    abstract protected function mapUserToObject(array $user);
166
167
    /**
168
     * Redirect the user of the application to the provider's authentication screen.
169
     *
170
     * @param string $redirectUrl
171
     *
172
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
173
     */
174
    public function redirect($redirectUrl = null)
175
    {
176
        $state = null;
177
178
        if (!is_null($redirectUrl)) {
179
            $this->redirectUrl = $redirectUrl;
180
        }
181
182
        if ($this->usesState()) {
183
            $state = $this->makeState();
184
        }
185
186
        return new RedirectResponse($this->getAuthUrl($state));
0 ignored issues
show
Bug introduced by
It seems like $state can also be of type false or null; however, Overtrue\Socialite\Provi...tProvider::getAuthUrl() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function user(AccessTokenInterface $token = null)
193
    {
194
        if (is_null($token) && $this->hasInvalidState()) {
195
            throw new InvalidStateException();
196
        }
197
198
        $token = $token ?: $this->getAccessToken($this->getCode());
199
200
        $user = $this->getUserByToken($token);
201
202
        $user = $this->mapUserToObject($user)->merge(['original' => $user]);
203
204
        return $user->setToken($token)->setProviderName($this->getName());
205
    }
206
207
    /**
208
     * Set redirect url.
209
     *
210
     * @param string $redirectUrl
211
     *
212
     * @return $this
213
     */
214
    public function setRedirectUrl($redirectUrl)
215
    {
216
        $this->redirectUrl = $redirectUrl;
217
218
        return $this;
219
    }
220
221
    /**
222
     * Set redirect url.
223
     *
224
     * @param string $redirectUrl
225
     *
226
     * @return $this
227
     */
228
    public function withRedirectUrl($redirectUrl)
229
    {
230
        $this->redirectUrl = $redirectUrl;
231
232
        return $this;
233
    }
234
235
    /**
236
     * Return the redirect url.
237
     *
238
     * @return string
239
     */
240
    public function getRedirectUrl()
241
    {
242
        return $this->redirectUrl;
243
    }
244
245
    /**
246
     * Set autoSaveState .
247
     *
248
     * @param bool $autoSaveState
249
     *
250
     * @return $this
251
     */
252
    public function setAutoSaveState(bool $autoSaveState)
253
    {
254
        $this->autoSaveState = $autoSaveState;
255
        return $this;
256
    }
257
    /**
258
     * @param \Overtrue\Socialite\AccessTokenInterface $accessToken
259
     *
260
     * @return $this
261
     */
262
    public function setAccessToken(AccessTokenInterface $accessToken)
263
    {
264
        $this->accessToken = $accessToken;
265
266
        return $this;
267
    }
268
269
    /**
270
     * Get the access token for the given code.
271
     *
272
     * @param string $code
273
     *
274
     * @return \Overtrue\Socialite\AccessTokenInterface
275
     */
276
    public function getAccessToken($code)
277
    {
278
        if ($this->accessToken) {
279
            return $this->accessToken;
280
        }
281
282
        $postKey = (1 === version_compare(ClientInterface::VERSION, '6')) ? 'form_params' : 'body';
283
284
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
285
            'headers' => ['Accept' => 'application/json'],
286
            $postKey => $this->getTokenFields($code),
287
        ]);
288
289
        return $this->parseAccessToken($response->getBody());
290
    }
291
292
    /**
293
     * Set the scopes of the requested access.
294
     *
295
     * @param array $scopes
296
     *
297
     * @return $this
298
     */
299
    public function scopes(array $scopes)
300
    {
301
        $this->scopes = $scopes;
302
303
        return $this;
304
    }
305
306
    /**
307
     * Set the request instance.
308
     *
309
     * @param Request $request
310
     *
311
     * @return $this
312
     */
313
    public function setRequest(Request $request)
0 ignored issues
show
Bug introduced by
You have injected the Request via parameter $request. This is generally not recommended as there might be multiple instances during a request cycle (f.e. when using sub-requests). Instead, it is recommended to inject the RequestStack and retrieve the current request each time you need it via getCurrentRequest().
Loading history...
314
    {
315
        $this->request = $request;
316
317
        return $this;
318
    }
319
320
    /**
321
     * Get the request instance.
322
     *
323
     * @return \Symfony\Component\HttpFoundation\Request
324
     */
325
    public function getRequest()
326
    {
327
        return $this->request;
328
    }
329
330
    /**
331
     * Indicates that the provider should operate as stateless.
332
     *
333
     * @return $this
334
     */
335
    public function stateless()
336
    {
337
        $this->stateless = true;
338
339
        return $this;
340
    }
341
342
    /**
343
     * Set the custom parameters of the request.
344
     *
345
     * @param array $parameters
346
     *
347
     * @return $this
348
     */
349
    public function with(array $parameters)
350
    {
351
        $this->parameters = $parameters;
352
353
        return $this;
354
    }
355
356
    /**
357
     * @throws \ReflectionException
358
     *
359
     * @return string
360
     */
361
    public function getName()
362
    {
363
        if (empty($this->name)) {
364
            $this->name = strstr((new \ReflectionClass(get_class($this)))->getShortName(), 'Provider', true);
365
        }
366
367
        return $this->name;
368
    }
369
370
    /**
371
     * Get the authentication URL for the provider.
372
     *
373
     * @param string $url
374
     * @param string $state
375
     *
376
     * @return string
377
     */
378
    protected function buildAuthUrlFromBase($url, $state)
379
    {
380
        return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType);
381
    }
382
383
    /**
384
     * Get the GET parameters for the code request.
385
     *
386
     * @param string|null $state
387
     *
388
     * @return array
389
     */
390 View Code Duplication
    protected function getCodeFields($state = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391
    {
392
        $fields = array_merge([
393
            'client_id' => $this->clientId,
394
            'redirect_uri' => $this->redirectUrl,
395
            'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator),
396
            'response_type' => 'code',
397
        ], $this->parameters);
398
399
        if ($this->usesState()) {
400
            $fields['state'] = $state;
401
        }
402
403
        return $fields;
404
    }
405
406
    /**
407
     * Format the given scopes.
408
     *
409
     * @param array  $scopes
410
     * @param string $scopeSeparator
411
     *
412
     * @return string
413
     */
414
    protected function formatScopes(array $scopes, $scopeSeparator)
415
    {
416
        return implode($scopeSeparator, $scopes);
417
    }
418
419
    /**
420
     * Determine if the current request / session has a mismatching "state".
421
     *
422
     * @return bool
423
     */
424
    protected function hasInvalidState()
425
    {
426
        if ($this->isStateless()) {
427
            return false;
428
        }
429
430
        $state = $this->request->getSession()->get('state');
431
        return !(strlen($state) > 0 && $this->request->get('state') === $state);
432
    }
433
434
    /**
435
     * Get the POST fields for the token request.
436
     *
437
     * @param string $code
438
     *
439
     * @return array
440
     */
441
    protected function getTokenFields($code)
442
    {
443
        return [
444
            'client_id' => $this->clientId,
445
            'client_secret' => $this->clientSecret,
446
            'code' => $code,
447
            'redirect_uri' => $this->redirectUrl,
448
        ];
449
    }
450
451
    /**
452
     * Get the access token from the token response body.
453
     *
454
     * @param \Psr\Http\Message\StreamInterface|array $body
455
     *
456
     * @return \Overtrue\Socialite\AccessTokenInterface
457
     */
458 View Code Duplication
    protected function parseAccessToken($body)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
459
    {
460
        if (!is_array($body)) {
461
            $body = json_decode($body, true);
462
        }
463
464
        if (empty($body['access_token'])) {
465
            throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body);
466
        }
467
468
        return new AccessToken($body);
469
    }
470
471
    /**
472
     * Get the code from the request.
473
     *
474
     * @return string
475
     */
476
    protected function getCode()
477
    {
478
        return $this->request->get('code');
479
    }
480
481
    /**
482
     * Get a fresh instance of the Guzzle HTTP client.
483
     *
484
     * @return \GuzzleHttp\Client
485
     */
486
    protected function getHttpClient()
487
    {
488
        return new Client(self::$guzzleOptions);
489
    }
490
491
    /**
492
     * Set options for Guzzle HTTP client.
493
     *
494
     * @param array $config
495
     *
496
     * @return array
497
     */
498
    public static function setGuzzleOptions($config = [])
499
    {
500
        return self::$guzzleOptions = $config;
501
    }
502
503
    /**
504
     * Determine if the provider is operating with state.
505
     *
506
     * @return bool
507
     */
508
    protected function usesState()
509
    {
510
        return !$this->stateless;
511
    }
512
513
    /**
514
     * Determine if the provider is operating as stateless.
515
     *
516
     * @return bool
517
     */
518
    protected function isStateless()
519
    {
520
        return !$this->request->hasSession() || $this->stateless;
521
    }
522
523
    /**
524
     * Return array item by key.
525
     *
526
     * @param array  $array
527
     * @param string $key
528
     * @param mixed  $default
529
     *
530
     * @return mixed
531
     */
532 View Code Duplication
    protected function arrayItem(array $array, $key, $default = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
533
    {
534
        if (is_null($key)) {
535
            return $array;
536
        }
537
538
        if (isset($array[$key])) {
539
            return $array[$key];
540
        }
541
542
        foreach (explode('.', $key) as $segment) {
543
            if (!is_array($array) || !array_key_exists($segment, $array)) {
544
                return $default;
545
            }
546
547
            $array = $array[$segment];
548
        }
549
550
        return $array;
551
    }
552
553
    /**
554
     * Put state to session storage and return it.
555
     *
556
     * @return string|bool
557
     */
558
    public function makeState()
559
    {
560
        $state = sha1(uniqid(mt_rand(1, 1000000), true));
561
        if (!$this->autoSaveState) {
562
            return $state;
563
        }
564
565
        if (!$this->request->hasSession()) {
566
            return false;
567
        }
568
569
        $session = $this->request->getSession();
570
571
        if (is_callable([$session, 'put'])) {
572
            $session->put('state', $state);
0 ignored issues
show
Bug introduced by
The method put() does not seem to exist on object<Symfony\Component...ssion\SessionInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
573
        } elseif (is_callable([$session, 'set'])) {
574
            $session->set('state', $state);
575
        } else {
576
            return false;
577
        }
578
579
        return $state;
580
    }
581
}
582