Completed
Push — master ( 03f078...b06b04 )
by Carlos
01:29
created

AbstractProvider::getName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
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\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
    /** Provider name
30
     *
31
     * @var string
32
     */
33
    protected $name;
34
35
    /**
36
     * The HTTP request instance.
37
     *
38
     * @var \Symfony\Component\HttpFoundation\Request
39
     */
40
    protected $request;
41
42
    /**
43
     * The client ID.
44
     *
45
     * @var string
46
     */
47
    protected $clientId;
48
49
    /**
50
     * The client secret.
51
     *
52
     * @var string
53
     */
54
    protected $clientSecret;
55
56
    /**
57
     * The redirect URL.
58
     *
59
     * @var string
60
     */
61
    protected $redirectUrl;
62
63
    /**
64
     * The custom parameters to be sent with the request.
65
     *
66
     * @var array
67
     */
68
    protected $parameters = [];
69
70
    /**
71
     * The scopes being requested.
72
     *
73
     * @var array
74
     */
75
    protected $scopes = [];
76
77
    /**
78
     * The separating character for the requested scopes.
79
     *
80
     * @var string
81
     */
82
    protected $scopeSeparator = ',';
83
84
    /**
85
     * The type of the encoding in the query.
86
     *
87
     * @var int Can be either PHP_QUERY_RFC3986 or PHP_QUERY_RFC1738
88
     */
89
    protected $encodingType = PHP_QUERY_RFC1738;
90
91
    /**
92
     * Indicates if the session state should be utilized.
93
     *
94
     * @var bool
95
     */
96
    protected $stateless = false;
97
98
    /**
99
     * Create a new provider instance.
100
     *
101
     * @param \Symfony\Component\HttpFoundation\Request $request
102
     * @param string                                    $clientId
103
     * @param string                                    $clientSecret
104
     * @param string|null                               $redirectUrl
105
     */
106
    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...
107
    {
108
        $this->request = $request;
109
        $this->clientId = $clientId;
110
        $this->clientSecret = $clientSecret;
111
        $this->redirectUrl = $redirectUrl;
112
    }
113
114
    /**
115
     * Get the authentication URL for the provider.
116
     *
117
     * @param string $state
118
     *
119
     * @return string
120
     */
121
    abstract protected function getAuthUrl($state);
122
123
    /**
124
     * Get the token URL for the provider.
125
     *
126
     * @return string
127
     */
128
    abstract protected function getTokenUrl();
129
130
    /**
131
     * Get the raw user for the given access token.
132
     *
133
     * @param \Overtrue\Socialite\AccessTokenInterface $token
134
     *
135
     * @return array
136
     */
137
    abstract protected function getUserByToken(AccessTokenInterface $token);
138
139
    /**
140
     * Map the raw user array to a Socialite User instance.
141
     *
142
     * @param array $user
143
     *
144
     * @return \Overtrue\Socialite\User
145
     */
146
    abstract protected function mapUserToObject(array $user);
147
148
    /**
149
     * Redirect the user of the application to the provider's authentication screen.
150
     *
151
     * @param string $redirectUrl
152
     *
153
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
154
     */
155
    public function redirect($redirectUrl = null)
156
    {
157
        $state = null;
158
159
        if (!is_null($redirectUrl)) {
160
            $this->redirectUrl = $redirectUrl;
161
        }
162
163
        if ($this->usesState()) {
164
            $state = $this->makeState();
165
        }
166
167
        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...
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173
    public function user(AccessTokenInterface $token = null)
174
    {
175
        if (is_null($token) && $this->hasInvalidState()) {
176
            throw new InvalidStateException();
177
        }
178
179
        $token = $token ?: $this->getAccessToken($this->getCode());
180
181
        $user = $this->getUserByToken($token);
182
183
        $user = $this->mapUserToObject($user)->merge(['original' => $user]);
184
185
        return $user->setToken($token)->setProvider($this->getName());
0 ignored issues
show
Bug introduced by
The method setProvider() does not exist on Overtrue\Socialite\User. Did you maybe mean setProviderName()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

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