Failed Conditions
Push — newinternal ( b66232...216d62 )
by Simon
16:33 queued 06:35
created

OAuthUserHelper   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 410
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 0
loc 410
rs 8.64
c 0
b 0
f 0
wmc 47
lcom 1
cbo 7

18 Methods

Rating   Name   Duplication   Size   Complexity  
A findUserByRequestToken() 0 22 2
A userIsFullyLinked() 0 8 3
A userIsPartiallyLinked() 0 13 5
A prepareTokenCountStatement() 0 6 2
A runTokenCount() 0 17 2
A __construct() 0 17 1
A isFullyLinked() 0 8 2
A isPartiallyLinked() 0 8 2
A refreshIdentity() 0 25 3
A getRequestToken() 0 22 1
A completeHandshake() 0 31 2
A detach() 0 12 1
A getIdentity() 0 10 2
A doApiCall() 0 8 1
B identityIsValid() 0 43 11
A identityExpired() 0 11 1
A loadIdentity() 0 23 3
A loadAccessToken() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like OAuthUserHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OAuthUserHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Helpers;
10
11
use DateTimeImmutable;
12
use PDOStatement;
13
use Waca\DataObjects\OAuthIdentity;
14
use Waca\DataObjects\OAuthToken;
15
use Waca\DataObjects\User;
16
use Waca\Exceptions\ApplicationLogicException;
17
use Waca\Exceptions\OAuthException;
18
use Waca\Helpers\Interfaces\IMediaWikiClient;
19
use Waca\Helpers\Interfaces\IOAuthProtocolHelper;
20
use Waca\PdoDatabase;
21
use Waca\SiteConfiguration;
22
23
class OAuthUserHelper implements IMediaWikiClient
24
{
25
    const TOKEN_REQUEST = 'request';
26
    const TOKEN_ACCESS = 'access';
27
    /** @var PDOStatement */
28
    private static $tokenCountStatement = null;
29
    /** @var PDOStatement */
30
    private $getTokenStatement;
31
    /**
32
     * @var User
33
     */
34
    private $user;
35
    /**
36
     * @var PdoDatabase
37
     */
38
    private $database;
39
    /**
40
     * @var IOAuthProtocolHelper
41
     */
42
    private $oauthProtocolHelper;
43
    /**
44
     * @var bool|null Is the user linked to OAuth
45
     */
46
    private $linked;
47
    private $partiallyLinked;
48
    /** @var OAuthToken */
49
    private $accessToken;
50
    /** @var bool */
51
    private $accessTokenLoaded = false;
52
    /**
53
     * @var OAuthIdentity
54
     */
55
    private $identity = null;
56
    /**
57
     * @var bool
58
     */
59
    private $identityLoaded = false;
60
    /**
61
     * @var SiteConfiguration
62
     */
63
    private $siteConfiguration;
64
65
    #region Static methods
66
    public static function findUserByRequestToken($requestToken, PdoDatabase $database)
67
    {
68
        $statement = $database->prepare(<<<'SQL'
69
            SELECT u.* FROM user u 
70
            INNER JOIN oauthtoken t ON t.user = u.id 
71
            WHERE t.type = :type AND t.token = :token
72
SQL
73
        );
74
        $statement->execute(array(':type' => self::TOKEN_REQUEST, ':token' => $requestToken));
75
76
        /** @var User $user */
77
        $user = $statement->fetchObject(User::class);
78
        $statement->closeCursor();
79
80
        if ($user === false) {
81
            throw new ApplicationLogicException('Token not found in store, please try again');
82
        }
83
84
        $user->setDatabase($database);
85
86
        return $user;
87
    }
88
89
    public static function userIsFullyLinked(User $user, PdoDatabase $database = null)
90
    {
91
        if (self::$tokenCountStatement === null && $database === null) {
92
            throw new ApplicationLogicException('Static link request without initialised statement');
93
        }
94
95
        return self::runTokenCount($user->getId(), $database, self::TOKEN_ACCESS);
96
    }
97
98
    public static function userIsPartiallyLinked(User $user, PdoDatabase $database = null)
99
    {
100
        if (self::$tokenCountStatement === null && $database === null) {
101
            throw new ApplicationLogicException('Static link request without initialised statement');
102
        }
103
104
        if (self::userIsFullyLinked($user, $database)) {
105
            return false;
106
        }
107
108
        return self::runTokenCount($user->getId(), $database, self::TOKEN_REQUEST)
109
            || $user->getOnWikiName() == null;
110
    }
111
112
    /**
113
     * @param PdoDatabase $database
114
     */
115
    public static function prepareTokenCountStatement(PdoDatabase $database)
116
    {
117
        if (self::$tokenCountStatement === null) {
118
            self::$tokenCountStatement = $database->prepare('SELECT COUNT(*) FROM oauthtoken WHERE user = :user AND type = :type');
119
        }
120
    }
121
122
    private static function runTokenCount($userId, $database, $tokenType)
123
    {
124
        if (self::$tokenCountStatement === null) {
125
            self::prepareTokenCountStatement($database);
126
        }
127
128
        self::$tokenCountStatement->execute(array(
129
            ':user' => $userId,
130
            ':type' => $tokenType,
131
        ));
132
133
        $tokenCount = self::$tokenCountStatement->fetchColumn();
134
        $linked = $tokenCount > 0;
135
        self::$tokenCountStatement->closeCursor();
136
137
        return $linked;
138
    }
139
140
    #endregion Static methods
141
142
    /**
143
     * OAuthUserHelper constructor.
144
     *
145
     * @param User                 $user
146
     * @param PdoDatabase          $database
147
     * @param IOAuthProtocolHelper $oauthProtocolHelper
148
     * @param SiteConfiguration    $siteConfiguration
149
     */
150
    public function __construct(
151
        User $user,
152
        PdoDatabase $database,
153
        IOAuthProtocolHelper $oauthProtocolHelper,
154
        SiteConfiguration $siteConfiguration
155
    ) {
156
        $this->user = $user;
157
        $this->database = $database;
158
        $this->oauthProtocolHelper = $oauthProtocolHelper;
159
160
        $this->linked = null;
161
        $this->partiallyLinked = null;
162
        $this->siteConfiguration = $siteConfiguration;
163
164
        self::prepareTokenCountStatement($database);
165
        $this->getTokenStatement = $this->database->prepare('SELECT * FROM oauthtoken WHERE user = :user AND type = :type');
166
    }
167
168
    /**
169
     * Determines if the user is fully connected to OAuth.
170
     *
171
     * @return bool
172
     */
173
    public function isFullyLinked()
174
    {
175
        if ($this->linked === null) {
176
            $this->linked = self::userIsFullyLinked($this->user, $this->database);
177
        }
178
179
        return $this->linked;
180
    }
181
182
    /**
183
     * Attempts to figure out if a user is partially linked to OAuth, and therefore needs to complete the OAuth
184
     * procedure before configuring.
185
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
186
     */
187
    public function isPartiallyLinked()
188
    {
189
        if ($this->partiallyLinked === null) {
190
            $this->partiallyLinked = self::userIsPartiallyLinked($this->user, $this->database);
191
        }
192
193
        return $this->partiallyLinked;
194
    }
195
196
    /**
197
     * @throws OAuthException
198
     */
199
    public function refreshIdentity()
200
    {
201
        $this->loadIdentity();
202
203
        if ($this->identity === null) {
204
            $this->identity = new OAuthIdentity();
205
            $this->identity->setUserId($this->user->getId());
206
            $this->identity->setDatabase($this->database);
207
        }
208
209
        $token = $this->loadAccessToken();
210
211
        $rawTicket = $this->oauthProtocolHelper->getIdentityTicket($token->getToken(), $token->getSecret());
212
213
        $this->identity->populate($rawTicket);
214
215
        if (!$this->identityIsValid()) {
216
            throw new OAuthException('Identity ticket is not valid!');
217
        }
218
219
        $this->identity->save();
220
221
        $this->user->setOnWikiName($this->identity->getUsername());
222
        $this->user->save();
223
    }
224
225
    public function getRequestToken()
226
    {
227
        $token = $this->oauthProtocolHelper->getRequestToken();
228
229
        $this->partiallyLinked = true;
230
        $this->linked = false;
231
232
        $this->database
233
            ->prepare('DELETE FROM oauthtoken WHERE user = :user AND type = :type')
234
            ->execute(array(':user' => $this->user->getId(), ':type' => self::TOKEN_REQUEST));
235
236
        $this->database
237
            ->prepare('INSERT INTO oauthtoken (user, type, token, secret, expiry) VALUES (:user, :type, :token, :secret, DATE_ADD(NOW(), INTERVAL 1 DAY))')
238
            ->execute(array(
239
                ':user'   => $this->user->getId(),
240
                ':type'   => self::TOKEN_REQUEST,
241
                ':token'  => $token->key,
242
                ':secret' => $token->secret,
243
            ));
244
245
        return $this->oauthProtocolHelper->getAuthoriseUrl($token->key);
246
    }
247
248
    public function completeHandshake($verificationToken)
249
    {
250
        $this->getTokenStatement->execute(array(':user' => $this->user->getId(), ':type' => self::TOKEN_REQUEST));
251
252
        /** @var OAuthToken $token */
253
        $token = $this->getTokenStatement->fetchObject(OAuthToken::class);
254
        $this->getTokenStatement->closeCursor();
255
256
        if ($token === false) {
257
            throw new ApplicationLogicException('Cannot find request token');
258
        }
259
260
        $token->setDatabase($this->database);
261
262
        $accessToken = $this->oauthProtocolHelper->callbackCompleted($token->getToken(), $token->getSecret(),
263
            $verificationToken);
264
265
        $clearStatement = $this->database->prepare('DELETE FROM oauthtoken WHERE user = :u AND type = :t');
266
        $clearStatement->execute(array(':u' => $this->user->getId(), ':t' => self::TOKEN_ACCESS));
267
268
        $token->setToken($accessToken->key);
269
        $token->setSecret($accessToken->secret);
270
        $token->setType(self::TOKEN_ACCESS);
271
        $token->setExpiry(null);
272
        $token->save();
273
274
        $this->partiallyLinked = false;
275
        $this->linked = true;
276
277
        $this->refreshIdentity();
278
    }
279
280
    public function detach()
281
    {
282
        $this->loadIdentity();
283
284
        $this->identity->delete();
285
        $statement = $this->database->prepare('DELETE FROM oauthtoken WHERE user = :user');
286
        $statement->execute(array(':user' => $this->user->getId()));
287
288
        $this->identity = null;
289
        $this->linked = false;
290
        $this->partiallyLinked = false;
291
    }
292
293
    /**
294
     * @param bool $expiredOk
295
     *
296
     * @return OAuthIdentity
297
     * @throws OAuthException
298
     */
299
    public function getIdentity($expiredOk = false)
300
    {
301
        $this->loadIdentity();
302
303
        if (!$this->identityIsValid($expiredOk)) {
304
            throw new OAuthException('Stored identity is not valid.');
305
        }
306
307
        return $this->identity;
308
    }
309
310
    public function doApiCall($params, $method)
311
    {
312
        // Ensure we're logged in
313
        $params['assert'] = 'user';
314
315
        $token = $this->loadAccessToken();
316
        return $this->oauthProtocolHelper->apiCall($params, $token->getToken(), $token->getSecret(), $method);
317
    }
318
319
    /**
320
     * @param bool $expiredOk
321
     *
322
     * @return bool
323
     */
324
    private function identityIsValid($expiredOk = false)
325
    {
326
        $this->loadIdentity();
327
328
        if ($this->identity === null) {
329
            return false;
330
        }
331
332
        if ($this->identity->getIssuedAtTime() === false
333
            || $this->identity->getExpirationTime() === false
334
            || $this->identity->getAudience() === false
335
            || $this->identity->getIssuer() === false
336
        ) {
337
            // this isn't populated properly.
338
            return false;
339
        }
340
341
        $issue = DateTimeImmutable::createFromFormat("U", $this->identity->getIssuedAtTime());
342
        $now = new DateTimeImmutable();
343
344
        if ($issue > $now) {
345
            // wat.
346
            return false;
347
        }
348
349
        if ($this->identityExpired() && !$expiredOk) {
350
            // soz.
351
            return false;
352
        }
353
354
        if ($this->identity->getAudience() !== $this->siteConfiguration->getOAuthConsumerToken()) {
355
            // token not issued for us
356
            return false;
357
        }
358
359
        if ($this->identity->getIssuer() !== $this->siteConfiguration->getOauthMediaWikiCanonicalServer()) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return !($this->identity...WikiCanonicalServer());.
Loading history...
360
            // token not issued by the right person
361
            return false;
362
        }
363
364
        // can't find a reason to not trust it
365
        return true;
366
    }
367
368
    /**
369
     * @return bool
370
     */
371
    public function identityExpired()
372
    {
373
        // allowed max age
374
        $gracePeriod = $this->siteConfiguration->getOauthIdentityGraceTime();
375
376
        $expiry = DateTimeImmutable::createFromFormat("U", $this->identity->getExpirationTime());
377
        $graceExpiry = $expiry->modify($gracePeriod);
378
        $now = new DateTimeImmutable();
379
380
        return $graceExpiry < $now;
381
    }
382
383
    /**
384
     * Loads the OAuth identity from the database for the current user.
385
     */
386
    private function loadIdentity()
387
    {
388
        if ($this->identityLoaded) {
389
            return;
390
        }
391
392
        $statement = $this->database->prepare('SELECT * FROM oauthidentity WHERE user = :user');
393
        $statement->execute(array(':user' => $this->user->getId()));
394
        /** @var OAuthIdentity $obj */
395
        $obj = $statement->fetchObject(OAuthIdentity::class);
396
397
        if ($obj === false) {
398
            // failed to load identity.
399
            $this->identityLoaded = true;
400
            $this->identity = null;
401
402
            return;
403
        }
404
405
        $obj->setDatabase($this->database);
406
        $this->identityLoaded = true;
407
        $this->identity = $obj;
408
    }
409
410
    /**
411
     * @return OAuthToken
412
     * @throws OAuthException
413
     */
414
    private function loadAccessToken()
415
    {
416
        if (!$this->accessTokenLoaded) {
417
            $this->getTokenStatement->execute(array(':user' => $this->user->getId(), ':type' => self::TOKEN_ACCESS));
418
            /** @var OAuthToken $token */
419
            $token = $this->getTokenStatement->fetchObject(OAuthToken::class);
420
            $this->getTokenStatement->closeCursor();
421
422
            if ($token === false) {
423
                throw new OAuthException('Access token not found!');
424
            }
425
426
            $this->accessToken = $token;
427
            $this->accessTokenLoaded = true;
428
        }
429
430
        return $this->accessToken;
431
    }
432
}