Completed
Pull Request — master (#74)
by
unknown
03:58
created

AbstractProvider::withRedirectUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
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
    /**
106
     * The options for guzzle\client
107
     *
108
     * @var array
109
     */
110
    protected static $guzzleOptions = [ 'http_errors' => false ];
111
112
113
    /**
114
     * Create a new provider instance.
115
     *
116
     * @param \Symfony\Component\HttpFoundation\Request $request
117
     * @param string                                    $clientId
118
     * @param string                                    $clientSecret
119
     * @param string|null                               $redirectUrl
120
     */
121
    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...
122
    {
123
        $this->request = $request;
124
        $this->clientId = $clientId;
125
        $this->clientSecret = $clientSecret;
126
        $this->redirectUrl = $redirectUrl;
127
    }
128
129
    /**
130
     * Get the authentication URL for the provider.
131
     *
132
     * @param string $state
133
     *
134
     * @return string
135
     */
136
    abstract protected function getAuthUrl($state);
137
138
    /**
139
     * Get the token URL for the provider.
140
     *
141
     * @return string
142
     */
143
    abstract protected function getTokenUrl();
144
145
    /**
146
     * Get the raw user for the given access token.
147
     *
148
     * @param \Overtrue\Socialite\AccessTokenInterface $token
149
     *
150
     * @return array
151
     */
152
    abstract protected function getUserByToken(AccessTokenInterface $token);
153
154
    /**
155
     * Map the raw user array to a Socialite User instance.
156
     *
157
     * @param array $user
158
     *
159
     * @return \Overtrue\Socialite\User
160
     */
161
    abstract protected function mapUserToObject(array $user);
162
163
    /**
164
     * Redirect the user of the application to the provider's authentication screen.
165
     *
166
     * @param string $redirectUrl
167
     *
168
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
169
     */
170
    public function redirect($redirectUrl = null)
171
    {
172
        $state = null;
173
174
        if (!is_null($redirectUrl)) {
175
            $this->redirectUrl = $redirectUrl;
176
        }
177
178
        if ($this->usesState()) {
179
            $state = $this->makeState();
180
        }
181
182
        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...
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188
    public function user(AccessTokenInterface $token = null)
189
    {
190
        if (is_null($token) && $this->hasInvalidState()) {
191
            throw new InvalidStateException();
192
        }
193
194
        $token = $token ?: $this->getAccessToken($this->getCode());
195
196
        $user = $this->getUserByToken($token);
197
198
        $user = $this->mapUserToObject($user)->merge(['original' => $user]);
199
200
        return $user->setToken($token)->setProviderName($this->getName());
201
    }
202
203
    /**
204
     * Set redirect url.
205
     *
206
     * @param string $redirectUrl
207
     *
208
     * @return $this
209
     */
210
    public function setRedirectUrl($redirectUrl)
211
    {
212
        $this->redirectUrl = $redirectUrl;
213
214
        return $this;
215
    }
216
217
    /**
218
     * Set redirect url.
219
     *
220
     * @param string $redirectUrl
221
     *
222
     * @return $this
223
     */
224
    public function withRedirectUrl($redirectUrl)
225
    {
226
        $this->redirectUrl = $redirectUrl;
227
228
        return $this;
229
    }
230
231
    /**
232
     * Return the redirect url.
233
     *
234
     * @return string
235
     */
236
    public function getRedirectUrl()
237
    {
238
        return $this->redirectUrl;
239
    }
240
241
    /**
242
     * @param \Overtrue\Socialite\AccessTokenInterface $accessToken
243
     *
244
     * @return $this
245
     */
246
    public function setAccessToken(AccessTokenInterface $accessToken)
247
    {
248
        $this->accessToken = $accessToken;
249
250
        return $this;
251
    }
252
253
    /**
254
     * Get the access token for the given code.
255
     *
256
     * @param string $code
257
     *
258
     * @return \Overtrue\Socialite\AccessTokenInterface
259
     */
260
    public function getAccessToken($code)
261
    {
262
        if ($this->accessToken) {
263
            return $this->accessToken;
264
        }
265
266
        $postKey = (version_compare(ClientInterface::VERSION, '6') === 1) ? 'form_params' : 'body';
267
268
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
269
            'headers' => ['Accept' => 'application/json'],
270
            $postKey => $this->getTokenFields($code),
271
        ]);
272
273
        return $this->parseAccessToken($response->getBody());
274
    }
275
276
    /**
277
     * Set the scopes of the requested access.
278
     *
279
     * @param array $scopes
280
     *
281
     * @return $this
282
     */
283
    public function scopes(array $scopes)
284
    {
285
        $this->scopes = $scopes;
286
287
        return $this;
288
    }
289
290
    /**
291
     * Set the request instance.
292
     *
293
     * @param Request $request
294
     *
295
     * @return $this
296
     */
297
    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...
298
    {
299
        $this->request = $request;
300
301
        return $this;
302
    }
303
304
    /**
305
     * Get the request instance.
306
     *
307
     * @return \Symfony\Component\HttpFoundation\Request
308
     */
309
    public function getRequest()
310
    {
311
        return $this->request;
312
    }
313
314
    /**
315
     * Indicates that the provider should operate as stateless.
316
     *
317
     * @return $this
318
     */
319
    public function stateless()
320
    {
321
        $this->stateless = true;
322
323
        return $this;
324
    }
325
326
    /**
327
     * Set the custom parameters of the request.
328
     *
329
     * @param array $parameters
330
     *
331
     * @return $this
332
     */
333
    public function with(array $parameters)
334
    {
335
        $this->parameters = $parameters;
336
337
        return $this;
338
    }
339
340
    /**
341
     * @return string
342
     */
343
    public function getName()
344
    {
345
        if (empty($this->name)) {
346
            $this->name = strstr((new \ReflectionClass(get_class($this)))->getShortName(), 'Provider', true);
347
        }
348
349
        return $this->name;
350
    }
351
352
    /**
353
     * Get the authentication URL for the provider.
354
     *
355
     * @param string $url
356
     * @param string $state
357
     *
358
     * @return string
359
     */
360
    protected function buildAuthUrlFromBase($url, $state)
361
    {
362
        return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType);
363
    }
364
365
    /**
366
     * Get the GET parameters for the code request.
367
     *
368
     * @param string|null $state
369
     *
370
     * @return array
371
     */
372
    protected function getCodeFields($state = null)
373
    {
374
        $fields = array_merge([
375
            'client_id' => $this->clientId,
376
            'redirect_uri' => $this->redirectUrl,
377
            'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator),
378
            'response_type' => 'code',
379
        ], $this->parameters);
380
381
        if ($this->usesState()) {
382
            $fields['state'] = $state;
383
        }
384
385
        return $fields;
386
    }
387
388
    /**
389
     * Format the given scopes.
390
     *
391
     * @param array  $scopes
392
     * @param string $scopeSeparator
393
     *
394
     * @return string
395
     */
396
    protected function formatScopes(array $scopes, $scopeSeparator)
397
    {
398
        return implode($scopeSeparator, $scopes);
399
    }
400
401
    /**
402
     * Determine if the current request / session has a mismatching "state".
403
     *
404
     * @return bool
405
     */
406
    protected function hasInvalidState()
407
    {
408
        if ($this->isStateless()) {
409
            return false;
410
        }
411
412
        $state = $this->request->getSession()->get('state');
413
414
        return !(strlen($state) > 0 && $this->request->get('state') === $state);
415
    }
416
417
    /**
418
     * Get the POST fields for the token request.
419
     *
420
     * @param string $code
421
     *
422
     * @return array
423
     */
424
    protected function getTokenFields($code)
425
    {
426
        return [
427
            'client_id' => $this->clientId,
428
            'client_secret' => $this->clientSecret,
429
            'code' => $code,
430
            'redirect_uri' => $this->redirectUrl,
431
        ];
432
    }
433
434
    /**
435
     * Get the access token from the token response body.
436
     *
437
     * @param \Psr\Http\Message\StreamInterface|array $body
438
     *
439
     * @return \Overtrue\Socialite\AccessTokenInterface
440
     */
441
    protected function parseAccessToken($body)
442
    {
443
        if (!is_array($body)) {
444
            $body = json_decode($body, true);
445
        }
446
447
        if (empty($body['access_token'])) {
448
            throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body);
449
        }
450
451
        return new AccessToken($body);
452
    }
453
454
    /**
455
     * Get the code from the request.
456
     *
457
     * @return string
458
     */
459
    protected function getCode()
460
    {
461
        return $this->request->get('code');
462
    }
463
464
    /**
465
     * Get a fresh instance of the Guzzle HTTP client.
466
     *
467
     * @return \GuzzleHttp\Client
468
     */
469
    protected function getHttpClient()
470
    {
471
        return new Client(self::$guzzleOptions);
472
    }
473
474
    /**
475
     * Set options for Guzzle HTTP client
476
     *
477
     * @param array $config
478
     */
479
    public static function setGuzzleOptions($config=[])
480
    {
481
        return self::$guzzleOptions = $config;
482
    }
483
484
    /**
485
     * Determine if the provider is operating with state.
486
     *
487
     * @return bool
488
     */
489
    protected function usesState()
490
    {
491
        return !$this->stateless;
492
    }
493
494
    /**
495
     * Determine if the provider is operating as stateless.
496
     *
497
     * @return bool
498
     */
499
    protected function isStateless()
500
    {
501
        return $this->stateless;
502
    }
503
504
    /**
505
     * Return array item by key.
506
     *
507
     * @param array  $array
508
     * @param string $key
509
     * @param mixed  $default
510
     *
511
     * @return mixed
512
     */
513 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...
514
    {
515
        if (is_null($key)) {
516
            return $array;
517
        }
518
519
        if (isset($array[$key])) {
520
            return $array[$key];
521
        }
522
523
        foreach (explode('.', $key) as $segment) {
524
            if (!is_array($array) || !array_key_exists($segment, $array)) {
525
                return $default;
526
            }
527
528
            $array = $array[$segment];
529
        }
530
531
        return $array;
532
    }
533
534
    /**
535
     * Put state to session storage and return it.
536
     *
537
     * @return string|bool
538
     */
539
    protected function makeState()
540
    {
541
        $state = sha1(uniqid(mt_rand(1, 1000000), true));
542
        $session = $this->request->getSession();
543
544
        if (is_callable([$session, 'put'])) {
545
            $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...
546
        } elseif (is_callable([$session, 'set'])) {
547
            $session->set('state', $state);
548
        } else {
549
            return false;
550
        }
551
552
        return $state;
553
    }
554
}
555