Completed
Push — master ( 6a3534...3888fe )
by Carlos
01:41
created

AbstractProvider::getConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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\Config;
20
use Overtrue\Socialite\InvalidStateException;
21
use Overtrue\Socialite\ProviderInterface;
22
use Symfony\Component\HttpFoundation\RedirectResponse;
23
use Symfony\Component\HttpFoundation\Request;
24
25
/**
26
 * Class AbstractProvider.
27
 */
28
abstract class AbstractProvider implements ProviderInterface
29
{
30
    /**
31
     * Provider name.
32
     *
33
     * @var string
34
     */
35
    protected $name;
36
37
    /**
38
     * The HTTP request instance.
39
     *
40
     * @var \Symfony\Component\HttpFoundation\Request
41
     */
42
    protected $request;
43
44
    /**
45
     * Driver config.
46
     *
47
     * @var Config
48
     */
49
    protected $config;
50
51
    /**
52
     * The client ID.
53
     *
54
     * @var string
55
     */
56
    protected $clientId;
57
58
    /**
59
     * The client secret.
60
     *
61
     * @var string
62
     */
63
    protected $clientSecret;
64
65
    /**
66
     * @var \Overtrue\Socialite\AccessTokenInterface
67
     */
68
    protected $accessToken;
69
70
    /**
71
     * The redirect URL.
72
     *
73
     * @var string
74
     */
75
    protected $redirectUrl;
76
77
    /**
78
     * The custom parameters to be sent with the request.
79
     *
80
     * @var array
81
     */
82
    protected $parameters = [];
83
84
    /**
85
     * The scopes being requested.
86
     *
87
     * @var array
88
     */
89
    protected $scopes = [];
90
91
    /**
92
     * The separating character for the requested scopes.
93
     *
94
     * @var string
95
     */
96
    protected $scopeSeparator = ',';
97
98
    /**
99
     * The type of the encoding in the query.
100
     *
101
     * @var int Can be either PHP_QUERY_RFC3986 or PHP_QUERY_RFC1738
102
     */
103
    protected $encodingType = PHP_QUERY_RFC1738;
104
105
    /**
106
     * Indicates if the session state should be utilized.
107
     *
108
     * @var bool
109
     */
110
    protected $stateless = false;
111
112
    /**
113
     * The options for guzzle\client.
114
     *
115
     * @var array
116
     */
117
    protected static $guzzleOptions = ['http_errors' => false];
118
119
    /**
120
     * Create a new provider instance.
121
     *
122
     * @param \Symfony\Component\HttpFoundation\Request $request
123
     * @param array                                     $config
124
     */
125
    public function __construct(Request $request, $config)
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->config = new Config($config);
128
        $this->request = $request;
129
        $this->redirectUrl = isset($config['redirect']) ? $config['redirect'] : null;
130
    }
131
132
    /**
133
     * Get the authentication URL for the provider.
134
     *
135
     * @param string $state
136
     *
137
     * @return string
138
     */
139
    abstract protected function getAuthUrl($state);
140
141
    /**
142
     * Get the token URL for the provider.
143
     *
144
     * @return string
145
     */
146
    abstract protected function getTokenUrl();
147
148
    /**
149
     * Get the raw user for the given access token.
150
     *
151
     * @param \Overtrue\Socialite\AccessTokenInterface $token
152
     *
153
     * @return array
154
     */
155
    abstract protected function getUserByToken(AccessTokenInterface $token);
156
157
    /**
158
     * Map the raw user array to a Socialite User instance.
159
     *
160
     * @param array $user
161
     *
162
     * @return \Overtrue\Socialite\User
163
     */
164
    abstract protected function mapUserToObject(array $user);
165
166
    /**
167
     * Redirect the user of the application to the provider's authentication screen.
168
     *
169
     * @param string $redirectUrl
170
     *
171
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
172
     */
173
    public function redirect($redirectUrl = null)
174
    {
175
        $state = null;
176
177
        if (!is_null($redirectUrl)) {
178
            $this->redirectUrl = $redirectUrl;
179
        }
180
181
        if ($this->usesState()) {
182
            $state = $this->makeState();
183
        }
184
185
        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...
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191 View Code Duplication
    public function user(AccessTokenInterface $token = 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...
192
    {
193
        if (is_null($token) && $this->hasInvalidState()) {
194
            throw new InvalidStateException();
195
        }
196
197
        $token = $token ?: $this->getAccessToken($this->getCode());
198
199
        $user = $this->getUserByToken($token);
200
201
        $user = $this->mapUserToObject($user)->merge(['original' => $user]);
202
203
        return $user->setToken($token)->setProviderName($this->getName());
204
    }
205
206
    /**
207
     * Set redirect url.
208
     *
209
     * @param string $redirectUrl
210
     *
211
     * @return $this
212
     */
213
    public function setRedirectUrl($redirectUrl)
214
    {
215
        $this->redirectUrl = $redirectUrl;
216
217
        return $this;
218
    }
219
220
    /**
221
     * Set redirect url.
222
     *
223
     * @param string $redirectUrl
224
     *
225
     * @return $this
226
     */
227
    public function withRedirectUrl($redirectUrl)
228
    {
229
        $this->redirectUrl = $redirectUrl;
230
231
        return $this;
232
    }
233
234
    /**
235
     * Return the redirect url.
236
     *
237
     * @return string
238
     */
239
    public function getRedirectUrl()
240
    {
241
        return $this->redirectUrl;
242
    }
243
244
    /**
245
     * @param \Overtrue\Socialite\AccessTokenInterface $accessToken
246
     *
247
     * @return $this
248
     */
249
    public function setAccessToken(AccessTokenInterface $accessToken)
250
    {
251
        $this->accessToken = $accessToken;
252
253
        return $this;
254
    }
255
256
    /**
257
     * Get the access token for the given code.
258
     *
259
     * @param string $code
260
     *
261
     * @return \Overtrue\Socialite\AccessTokenInterface
262
     */
263
    public function getAccessToken($code)
264
    {
265
        if ($this->accessToken) {
266
            return $this->accessToken;
267
        }
268
269
        $postKey = (1 === version_compare(ClientInterface::VERSION, '6')) ? 'form_params' : 'body';
0 ignored issues
show
Deprecated Code introduced by
The constant GuzzleHttp\ClientInterface::VERSION has been deprecated with message: Will be removed in Guzzle 7.0.0

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

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