Completed
Pull Request — master (#74)
by
unknown
01:32
created

AbstractProvider::setGuzzleOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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