Completed
Push — master ( 15b1ce...1cbe87 )
by Steven
02:32
created

LinkedIn::checkResponseUnauthorized()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.9332
c 0
b 0
f 0
cc 4
nc 2
nop 2
crap 4
1
<?php
2
3
namespace League\OAuth2\Client\Provider;
4
5
use Exception;
6
use InvalidArgumentException;
7
use League\OAuth2\Client\Grant\AbstractGrant;
8
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
9
use League\OAuth2\Client\Provider\Exception\LinkedInAccessDeniedException;
10
use League\OAuth2\Client\Token\AccessToken;
11
use League\OAuth2\Client\Token\LinkedInAccessToken;
12
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
13
use Psr\Http\Message\ResponseInterface;
14
15
class LinkedIn extends AbstractProvider
16
{
17
    use BearerAuthorizationTrait;
18
19
    /**
20
     * Default scopes
21
     *
22
     * @var array
23
     */
24
    public $defaultScopes = ['r_liteprofile', 'r_emailaddress'];
25
26
    /**
27
     * Requested fields in scope, seeded with default values
28
     *
29
     * @var array
30
     * @see https://developer.linkedin.com/docs/fields/basic-profile
31
     */
32
    protected $fields = [
33
        'id', 'firstName', 'lastName', 'localizedFirstName', 'localizedLastName',
34
        'profilePicture(displayImage~:playableStreams)',
35
    ];
36
37
    /**
38
     * Constructs an OAuth 2.0 service provider.
39
     *
40
     * @param array $options An array of options to set on this provider.
41
     *     Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
42
     *     Individual providers may introduce more options, as needed.
43
     * @param array $collaborators An array of collaborators that may be used to
44
     *     override this provider's default behavior. Collaborators include
45
     *     `grantFactory`, `requestFactory`, and `httpClient`.
46
     *     Individual providers may introduce more collaborators, as needed.
47
     */
48 16
    public function __construct(array $options = [], array $collaborators = [])
49
    {
50 16
        if (isset($options['fields']) && !is_array($options['fields'])) {
51 1
            throw new InvalidArgumentException('The fields option must be an array');
52
        }
53
54 16
        parent::__construct($options, $collaborators);
55 16
    }
56
57
58
    /**
59
     * Creates an access token from a response.
60
     *
61
     * The grant that was used to fetch the response can be used to provide
62
     * additional context.
63
     *
64
     * @param  array $response
65
     * @param  AbstractGrant $grant
66
     * @return AccessTokenInterface
67
     */
68 7
    protected function createAccessToken(array $response, AbstractGrant $grant)
69
    {
70 7
        return new LinkedInAccessToken($response);
71
    }
72
73
    /**
74
     * Get the string used to separate scopes.
75
     *
76
     * @return string
77
     */
78 3
    protected function getScopeSeparator()
79
    {
80 3
        return ' ';
81
    }
82
83
    /**
84
     * Get authorization url to begin OAuth flow
85
     *
86
     * @return string
87
     */
88 3
    public function getBaseAuthorizationUrl()
89
    {
90 3
        return 'https://www.linkedin.com/oauth/v2/authorization';
91
    }
92
93
    /**
94
     * Get access token url to retrieve token
95
     *
96
     * @return string
97
     */
98 9
    public function getBaseAccessTokenUrl(array $params)
99
    {
100 9
        return 'https://www.linkedin.com/oauth/v2/accessToken';
101
    }
102
103
    /**
104
     * Get provider url to fetch user details
105
     *
106
     * @param  AccessToken $token
107
     *
108
     * @return string
109
     */
110 4
    public function getResourceOwnerDetailsUrl(AccessToken $token)
111
    {
112 4
        $query = http_build_query([
113 4
            'projection' => '(' . implode(',', $this->fields) . ')'
114
        ]);
115
116 4
        return 'https://api.linkedin.com/v2/me?' . urldecode($query);
117
    }
118
119
    /**
120
     * Get provider url to fetch user details
121
     *
122
     * @param  AccessToken $token
123
     *
124
     * @return string
125
     */
126 7
    public function getResourceOwnerEmailUrl(AccessToken $token)
0 ignored issues
show
Unused Code introduced by
The parameter $token is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
127
    {
128 7
        $query = http_build_query([
129 7
            'q' => 'members',
130
            'projection' => '(elements*(state,primary,type,handle~))'
131
        ]);
132
133 7
        return 'https://api.linkedin.com/v2/clientAwareMemberHandles?' . urldecode($query);
134
    }
135
136
    /**
137
     * Get the default scopes used by this provider.
138
     *
139
     * This should not be a complete list of all scopes, but the minimum
140
     * required for the provider user interface!
141
     *
142
     * @return array
143
     */
144 2
    protected function getDefaultScopes()
145
    {
146 2
        return $this->defaultScopes;
147
    }
148
149
    /**
150
     * Check a provider response for errors.
151
     *
152
     * @param  ResponseInterface $response
153
     * @param  array $data Parsed response data
154
     * @return void
155
     * @throws IdentityProviderException
156
     * @see https://developer.linkedin.com/docs/guide/v2/error-handling
157
     */
158 8
    protected function checkResponse(ResponseInterface $response, $data)
159
    {
160 8
        $this->checkResponseUnauthorized($response, $data);
161
162 8
        if ($response->getStatusCode() >= 400) {
163 1
            throw new IdentityProviderException(
164 1
                $data['message'] ?: $response->getReasonPhrase(),
165 1
                $data['status'] ?: $response->getStatusCode(),
166 1
                $response
0 ignored issues
show
Documentation introduced by
$response is of type object<Psr\Http\Message\ResponseInterface>, but the function expects a array|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
167
            );
168
        }
169 7
    }
170
171
    /**
172
     * Check a provider response for unauthorized errors.
173
     *
174
     * @param  ResponseInterface $response
175
     * @param  array $data Parsed response data
176
     * @return void
177
     * @throws LinkedInAccessDeniedException
178
     * @see https://developer.linkedin.com/docs/guide/v2/error-handling
179
     */
180 8
    protected function checkResponseUnauthorized(ResponseInterface $response, $data)
181
    {
182 8
        if (isset($data['status']) && $data['status'] === 403) {
183 2
            throw new LinkedInAccessDeniedException(
184 2
                $data['message'] ?: $response->getReasonPhrase(),
185 2
                $response->getStatusCode(),
186 2
                $response
0 ignored issues
show
Documentation introduced by
$response is of type object<Psr\Http\Message\ResponseInterface>, but the function expects a array|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
187
            );
188
        }
189 8
    }
190
191
    /**
192
     * Generate a user object from a successful user details request.
193
     *
194
     * @param array $response
195
     * @param AccessToken $token
196
     * @return LinkedInResourceOwner
197
     */
198 3
    protected function createResourceOwner(array $response, AccessToken $token)
199
    {
200
        // If current accessToken is not authorized with r_emailaddress scope,
201
        // getResourceOwnerEmail will throw LinkedInAccessDeniedException, it will be caught here,
202
        // and then the email will be set to null
203
        // When email is not available due to chosen scopes, other providers simply set it to null, let's do the same.
204
        try {
205 3
            $email = $this->getResourceOwnerEmail($token);
206 1
        } catch (LinkedInAccessDeniedException $exception) {
207 1
            $email = null;
208
        }
209
210 3
        return new LinkedInResourceOwner($response, $email);
211
    }
212
213
    /**
214
     * Returns the requested fields in scope.
215
     *
216
     * @return array
217
     */
218 3
    public function getFields()
219
    {
220 3
        return $this->fields;
221
    }
222
223
    /**
224
     * Attempts to fetch resource owner's email address via separate API request.
225
     *
226
     * @param  AccessToken $token [description]
227
     * @return string|null
228
     * @throws IdentityProviderException
229
     */
230 6
    public function getResourceOwnerEmail(AccessToken $token)
231
    {
232 6
        $emailUrl = $this->getResourceOwnerEmailUrl($token);
233 6
        $emailRequest = $this->getAuthenticatedRequest(self::METHOD_GET, $emailUrl, $token);
234 6
        $emailResponse = $this->getParsedResponse($emailRequest);
235
236 4
        return $this->extractEmailFromResponse($emailResponse);
0 ignored issues
show
Bug introduced by
It seems like $emailResponse defined by $this->getParsedResponse($emailRequest) on line 234 can also be of type null or string; however, League\OAuth2\Client\Pro...ractEmailFromResponse() does only seem to accept array, 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...
237
    }
238
239
    /**
240
     * Updates the requested fields in scope.
241
     *
242
     * @param  array   $fields
243
     *
244
     * @return LinkedIn
245
     */
246 1
    public function withFields(array $fields)
247
    {
248 1
        $this->fields = $fields;
249
250 1
        return $this;
251
    }
252
253
    /**
254
     * Attempts to extract the email address from a valid email api response.
255
     *
256
     * @param  array  $response
257
     * @return string|null
258
     */
259
    protected function extractEmailFromResponse($response = [])
260
    {
261
        try {
262 4
            $confirmedEmails = array_filter($response['elements'], function ($element) {
263
                return
264 3
                    strtoupper($element['type']) === 'EMAIL'
265 3
                    && strtoupper($element['state']) === 'CONFIRMED'
266 3
                    && $element['primary'] === true
267 3
                    && isset($element['handle~']['emailAddress'])
268
                ;
269 4
            });
270
271 3
            return $confirmedEmails[0]['handle~']['emailAddress'];
272 1
        } catch (Exception $e) {
273 1
            return null;
274
        }
275
    }
276
}
277