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

AbstractProvider::setAutoSaveState()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
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
256
        return $this;
257
    }
258
259
    /**
260
     * @param \Overtrue\Socialite\AccessTokenInterface $accessToken
261
     *
262
     * @return $this
263
     */
264
    public function setAccessToken(AccessTokenInterface $accessToken)
265
    {
266
        $this->accessToken = $accessToken;
267
268
        return $this;
269
    }
270
271
    /**
272
     * Get the access token for the given code.
273
     *
274
     * @param string $code
275
     *
276
     * @return \Overtrue\Socialite\AccessTokenInterface
277
     */
278
    public function getAccessToken($code)
279
    {
280
        if ($this->accessToken) {
281
            return $this->accessToken;
282
        }
283
284
        $postKey = (1 === version_compare(ClientInterface::VERSION, '6')) ? 'form_params' : 'body';
285
286
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
287
            'headers' => ['Accept' => 'application/json'],
288
            $postKey => $this->getTokenFields($code),
289
        ]);
290
291
        return $this->parseAccessToken($response->getBody());
292
    }
293
294
    /**
295
     * Set the scopes of the requested access.
296
     *
297
     * @param array $scopes
298
     *
299
     * @return $this
300
     */
301
    public function scopes(array $scopes)
302
    {
303
        $this->scopes = $scopes;
304
305
        return $this;
306
    }
307
308
    /**
309
     * Set the request instance.
310
     *
311
     * @param Request $request
312
     *
313
     * @return $this
314
     */
315
    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...
316
    {
317
        $this->request = $request;
318
319
        return $this;
320
    }
321
322
    /**
323
     * Get the request instance.
324
     *
325
     * @return \Symfony\Component\HttpFoundation\Request
326
     */
327
    public function getRequest()
328
    {
329
        return $this->request;
330
    }
331
332
    /**
333
     * Indicates that the provider should operate as stateless.
334
     *
335
     * @return $this
336
     */
337
    public function stateless()
338
    {
339
        $this->stateless = true;
340
341
        return $this;
342
    }
343
344
    /**
345
     * Set the custom parameters of the request.
346
     *
347
     * @param array $parameters
348
     *
349
     * @return $this
350
     */
351
    public function with(array $parameters)
352
    {
353
        $this->parameters = $parameters;
354
355
        return $this;
356
    }
357
358
    /**
359
     * @throws \ReflectionException
360
     *
361
     * @return string
362
     */
363
    public function getName()
364
    {
365
        if (empty($this->name)) {
366
            $this->name = strstr((new \ReflectionClass(get_class($this)))->getShortName(), 'Provider', true);
367
        }
368
369
        return $this->name;
370
    }
371
372
    /**
373
     * Get the authentication URL for the provider.
374
     *
375
     * @param string $url
376
     * @param string $state
377
     *
378
     * @return string
379
     */
380
    protected function buildAuthUrlFromBase($url, $state)
381
    {
382
        return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType);
383
    }
384
385
    /**
386
     * Get the GET parameters for the code request.
387
     *
388
     * @param string|null $state
389
     *
390
     * @return array
391
     */
392 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...
393
    {
394
        $fields = array_merge([
395
            'client_id' => $this->clientId,
396
            'redirect_uri' => $this->redirectUrl,
397
            'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator),
398
            'response_type' => 'code',
399
        ], $this->parameters);
400
401
        if ($this->usesState()) {
402
            $fields['state'] = $state;
403
        }
404
405
        return $fields;
406
    }
407
408
    /**
409
     * Format the given scopes.
410
     *
411
     * @param array  $scopes
412
     * @param string $scopeSeparator
413
     *
414
     * @return string
415
     */
416
    protected function formatScopes(array $scopes, $scopeSeparator)
417
    {
418
        return implode($scopeSeparator, $scopes);
419
    }
420
421
    /**
422
     * Determine if the current request / session has a mismatching "state".
423
     *
424
     * @return bool
425
     */
426
    protected function hasInvalidState()
427
    {
428
        if ($this->isStateless()) {
429
            return false;
430
        }
431
432
        $state = $this->request->getSession()->get('state');
433
434
        return !(strlen($state) > 0 && $this->request->get('state') === $state);
435
    }
436
437
    /**
438
     * Get the POST fields for the token request.
439
     *
440
     * @param string $code
441
     *
442
     * @return array
443
     */
444
    protected function getTokenFields($code)
445
    {
446
        return [
447
            'client_id' => $this->clientId,
448
            'client_secret' => $this->clientSecret,
449
            'code' => $code,
450
            'redirect_uri' => $this->redirectUrl,
451
        ];
452
    }
453
454
    /**
455
     * Get the access token from the token response body.
456
     *
457
     * @param \Psr\Http\Message\StreamInterface|array $body
458
     *
459
     * @return \Overtrue\Socialite\AccessTokenInterface
460
     */
461 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...
462
    {
463
        if (!is_array($body)) {
464
            $body = json_decode($body, true);
465
        }
466
467
        if (empty($body['access_token'])) {
468
            throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body);
469
        }
470
471
        return new AccessToken($body);
472
    }
473
474
    /**
475
     * Get the code from the request.
476
     *
477
     * @return string
478
     */
479
    protected function getCode()
480
    {
481
        return $this->request->get('code');
482
    }
483
484
    /**
485
     * Get a fresh instance of the Guzzle HTTP client.
486
     *
487
     * @return \GuzzleHttp\Client
488
     */
489
    protected function getHttpClient()
490
    {
491
        return new Client(self::$guzzleOptions);
492
    }
493
494
    /**
495
     * Set options for Guzzle HTTP client.
496
     *
497
     * @param array $config
498
     *
499
     * @return array
500
     */
501
    public static function setGuzzleOptions($config = [])
502
    {
503
        return self::$guzzleOptions = $config;
504
    }
505
506
    /**
507
     * Determine if the provider is operating with state.
508
     *
509
     * @return bool
510
     */
511
    protected function usesState()
512
    {
513
        return !$this->stateless;
514
    }
515
516
    /**
517
     * Determine if the provider is operating as stateless.
518
     *
519
     * @return bool
520
     */
521
    protected function isStateless()
522
    {
523
        return !$this->request->hasSession() || $this->stateless;
524
    }
525
526
    /**
527
     * Return array item by key.
528
     *
529
     * @param array  $array
530
     * @param string $key
531
     * @param mixed  $default
532
     *
533
     * @return mixed
534
     */
535 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...
536
    {
537
        if (is_null($key)) {
538
            return $array;
539
        }
540
541
        if (isset($array[$key])) {
542
            return $array[$key];
543
        }
544
545
        foreach (explode('.', $key) as $segment) {
546
            if (!is_array($array) || !array_key_exists($segment, $array)) {
547
                return $default;
548
            }
549
550
            $array = $array[$segment];
551
        }
552
553
        return $array;
554
    }
555
556
    /**
557
     * Put state to session storage and return it.
558
     *
559
     * @return string|bool
560
     */
561
    public function makeState()
562
    {
563
        $state = sha1(uniqid(mt_rand(1, 1000000), true));
564
        if (!$this->autoSaveState) {
565
            return $state;
566
        }
567
568
        if (!$this->request->hasSession()) {
569
            return false;
570
        }
571
572
        $session = $this->request->getSession();
573
574
        if (is_callable([$session, 'put'])) {
575
            $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...
576
        } elseif (is_callable([$session, 'set'])) {
577
            $session->set('state', $state);
578
        } else {
579
            return false;
580
        }
581
582
        return $state;
583
    }
584
}
585