Completed
Pull Request — master (#68)
by mingyoung
01:27
created

AbstractProvider::getTokenUrl()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1
c 0
b 0
f 0
nc 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\AccessToken
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
     * Create a new provider instance.
106
     *
107
     * @param \Symfony\Component\HttpFoundation\Request $request
108
     * @param string                                    $clientId
109
     * @param string                                    $clientSecret
110
     * @param string|null                               $redirectUrl
111
     */
112
    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...
113
    {
114
        $this->request = $request;
115
        $this->clientId = $clientId;
116
        $this->clientSecret = $clientSecret;
117
        $this->redirectUrl = $redirectUrl;
118
    }
119
120
    /**
121
     * Get the authentication URL for the provider.
122
     *
123
     * @param string $state
124
     *
125
     * @return string
126
     */
127
    abstract protected function getAuthUrl($state);
128
129
    /**
130
     * Get the token URL for the provider.
131
     *
132
     * @return string
133
     */
134
    abstract protected function getTokenUrl();
135
136
    /**
137
     * Get the raw user for the given access token.
138
     *
139
     * @param \Overtrue\Socialite\AccessTokenInterface $token
140
     *
141
     * @return array
142
     */
143
    abstract protected function getUserByToken(AccessTokenInterface $token);
144
145
    /**
146
     * Map the raw user array to a Socialite User instance.
147
     *
148
     * @param array $user
149
     *
150
     * @return \Overtrue\Socialite\User
151
     */
152
    abstract protected function mapUserToObject(array $user);
153
154
    /**
155
     * Redirect the user of the application to the provider's authentication screen.
156
     *
157
     * @param string $redirectUrl
158
     *
159
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
160
     */
161
    public function redirect($redirectUrl = null)
162
    {
163
        $state = null;
164
165
        if (!is_null($redirectUrl)) {
166
            $this->redirectUrl = $redirectUrl;
167
        }
168
169
        if ($this->usesState()) {
170
            $state = $this->makeState();
171
        }
172
173
        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...
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function user(AccessTokenInterface $token = null)
180
    {
181
        if (is_null($token) && $this->hasInvalidState()) {
182
            throw new InvalidStateException();
183
        }
184
185
        $token = $token ?: $this->getAccessToken($this->getCode());
186
187
        $user = $this->getUserByToken($token);
188
189
        $user = $this->mapUserToObject($user)->merge(['original' => $user]);
190
191
        return $user->setToken($token)->setProviderName($this->getName());
192
    }
193
194
    /**
195
     * Set redirect url.
196
     *
197
     * @param string $redirectUrl
198
     *
199
     * @return $this
200
     */
201
    public function setRedirectUrl($redirectUrl)
202
    {
203
        $this->redirectUrl = $redirectUrl;
204
205
        return $this;
206
    }
207
208
    /**
209
     * Set redirect url.
210
     *
211
     * @param string $redirectUrl
212
     *
213
     * @return $this
214
     */
215
    public function withRedirectUrl($redirectUrl)
216
    {
217
        $this->redirectUrl = $redirectUrl;
218
219
        return $this;
220
    }
221
222
    /**
223
     * Return the redirect url.
224
     *
225
     * @return string
226
     */
227
    public function getRedirectUrl()
228
    {
229
        return $this->redirectUrl;
230
    }
231
232
    /**
233
     * @param \Overtrue\Socialite\AccessTokenInterface $accessToken
234
     *
235
     * @return $this
236
     */
237
    public function setAccessToken(AccessTokenInterface $accessToken)
238
    {
239
        $this->accessToken = $accessToken;
0 ignored issues
show
Documentation Bug introduced by
$accessToken is of type object<Overtrue\Socialite\AccessTokenInterface>, but the property $accessToken was declared to be of type object<Overtrue\Socialite\AccessToken>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

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