OAuthUserHelper   D
last analyzed

Complexity

Total Complexity 59

Size/Duplication

Total Lines 457
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 59
eloc 181
dl 0
loc 457
rs 4.08
c 2
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A completeHandshake() 0 30 2
A userIsFullyLinked() 0 7 3
A canCreateAccount() 0 6 4
A __construct() 0 18 1
A canWelcome() 0 6 4
C identityIsValid() 0 46 13
A getRequestToken() 0 21 1
A runTokenCount() 0 16 2
A userIsPartiallyLinked() 0 12 5
A isPartiallyLinked() 0 7 2
A findUserByRequestToken() 0 21 2
A refreshIdentity() 0 33 5
A prepareTokenCountStatement() 0 4 2
A getIdentity() 0 9 2
A loadIdentity() 0 22 3
A detach() 0 11 1
A doApiCall() 0 7 1
A loadAccessToken() 0 17 3
A identityExpired() 0 10 1
A isFullyLinked() 0 7 2

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.

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 MediaWiki\OAuthClient\Exception;
13
use PDOStatement;
14
use Waca\DataObjects\OAuthIdentity;
15
use Waca\DataObjects\OAuthToken;
16
use Waca\DataObjects\User;
17
use Waca\Exceptions\ApplicationLogicException;
18
use Waca\Exceptions\CurlException;
19
use Waca\Exceptions\OAuthException;
20
use Waca\Exceptions\OptimisticLockFailedException;
21
use Waca\Helpers\Interfaces\IMediaWikiClient;
22
use Waca\Helpers\Interfaces\IOAuthProtocolHelper;
23
use Waca\PdoDatabase;
24
use Waca\SiteConfiguration;
25
26
class OAuthUserHelper implements IMediaWikiClient
27
{
28
    const TOKEN_REQUEST = 'request';
29
    const TOKEN_ACCESS = 'access';
30
    /** @var PDOStatement */
31
    private static $tokenCountStatement = null;
32
    /** @var PDOStatement */
33
    private $getTokenStatement;
34
    /**
35
     * @var User
36
     */
37
    private $user;
38
    /**
39
     * @var PdoDatabase
40
     */
41
    private $database;
42
    /**
43
     * @var IOAuthProtocolHelper
44
     */
45
    private $oauthProtocolHelper;
46
    /**
47
     * @var bool|null Is the user linked to OAuth
48
     */
49
    private $linked;
50
    private $partiallyLinked;
51
    /** @var OAuthToken */
52
    private $accessToken;
53
    /** @var bool */
54
    private $accessTokenLoaded = false;
55
    /**
56
     * @var OAuthIdentity
57
     */
58
    private $identity = null;
59
    /**
60
     * @var bool
61
     */
62
    private $identityLoaded = false;
63
    /**
64
     * @var SiteConfiguration
65
     */
66
    private $siteConfiguration;
67
68
    private $legacyTokens;
69
70
    #region Static methods
71
72
    public static function findUserByRequestToken($requestToken, PdoDatabase $database)
73
    {
74
        $statement = $database->prepare(<<<'SQL'
75
            SELECT u.* FROM user u 
76
            INNER JOIN oauthtoken t ON t.user = u.id 
77
            WHERE t.type = :type AND t.token = :token
78
SQL
79
        );
80
        $statement->execute(array(':type' => self::TOKEN_REQUEST, ':token' => $requestToken));
81
82
        /** @var User $user */
83
        $user = $statement->fetchObject(User::class);
84
        $statement->closeCursor();
85
86
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
87
            throw new ApplicationLogicException('Token not found in store, please try again');
88
        }
89
90
        $user->setDatabase($database);
91
92
        return $user;
93
    }
94
95
    public static function userIsFullyLinked(User $user, PdoDatabase $database = null)
96
    {
97
        if (self::$tokenCountStatement === null && $database === null) {
98
            throw new ApplicationLogicException('Static link request without initialised statement');
99
        }
100
101
        return self::runTokenCount($user->getId(), $database, self::TOKEN_ACCESS);
102
    }
103
104
    public static function userIsPartiallyLinked(User $user, PdoDatabase $database = null)
105
    {
106
        if (self::$tokenCountStatement === null && $database === null) {
107
            throw new ApplicationLogicException('Static link request without initialised statement');
108
        }
109
110
        if (self::userIsFullyLinked($user, $database)) {
111
            return false;
112
        }
113
114
        return self::runTokenCount($user->getId(), $database, self::TOKEN_REQUEST)
115
            || $user->getOnWikiName() == null;
116
    }
117
118
    /**
119
     * @param PdoDatabase $database
120
     */
121
    public static function prepareTokenCountStatement(PdoDatabase $database)
122
    {
123
        if (self::$tokenCountStatement === null) {
124
            self::$tokenCountStatement = $database->prepare('SELECT COUNT(*) FROM oauthtoken WHERE user = :user AND type = :type');
125
        }
126
    }
127
128
    private static function runTokenCount($userId, $database, $tokenType)
129
    {
130
        if (self::$tokenCountStatement === null) {
131
            self::prepareTokenCountStatement($database);
132
        }
133
134
        self::$tokenCountStatement->execute(array(
135
            ':user' => $userId,
136
            ':type' => $tokenType,
137
        ));
138
139
        $tokenCount = self::$tokenCountStatement->fetchColumn();
140
        $linked = $tokenCount > 0;
141
        self::$tokenCountStatement->closeCursor();
142
143
        return $linked;
144
    }
145
146
    #endregion Static methods
147
148
    /**
149
     * OAuthUserHelper constructor.
150
     *
151
     * @param User                 $user
152
     * @param PdoDatabase          $database
153
     * @param IOAuthProtocolHelper $oauthProtocolHelper
154
     * @param SiteConfiguration    $siteConfiguration
155
     */
156
    public function __construct(
157
        User $user,
158
        PdoDatabase $database,
159
        IOAuthProtocolHelper $oauthProtocolHelper,
160
        SiteConfiguration $siteConfiguration
161
    ) {
162
        $this->user = $user;
163
        $this->database = $database;
164
        $this->oauthProtocolHelper = $oauthProtocolHelper;
165
166
        $this->linked = null;
167
        $this->partiallyLinked = null;
168
        $this->siteConfiguration = $siteConfiguration;
169
170
        self::prepareTokenCountStatement($database);
171
        $this->getTokenStatement = $this->database->prepare('SELECT * FROM oauthtoken WHERE user = :user AND type = :type');
172
173
        $this->legacyTokens = $this->siteConfiguration->getOauthLegacyConsumerTokens();
174
    }
175
176
    /**
177
     * Determines if the user is fully connected to OAuth.
178
     *
179
     * @return bool
180
     */
181
    public function isFullyLinked()
182
    {
183
        if ($this->linked === null) {
184
            $this->linked = self::userIsFullyLinked($this->user, $this->database);
185
        }
186
187
        return $this->linked;
188
    }
189
190
    /**
191
     * Attempts to figure out if a user is partially linked to OAuth, and therefore needs to complete the OAuth
192
     * procedure before configuring.
193
     * @return bool
194
     */
195
    public function isPartiallyLinked()
196
    {
197
        if ($this->partiallyLinked === null) {
198
            $this->partiallyLinked = self::userIsPartiallyLinked($this->user, $this->database);
199
        }
200
201
        return $this->partiallyLinked;
202
    }
203
204
    public function canCreateAccount()
205
    {
206
        return $this->isFullyLinked()
207
            && $this->getIdentity(true)->getGrantBasic()
208
            && $this->getIdentity(true)->getGrantHighVolume()
209
            && $this->getIdentity(true)->getGrantCreateAccount();
210
    }
211
212
    public function canWelcome()
213
    {
214
        return $this->isFullyLinked()
215
            && $this->getIdentity(true)->getGrantBasic()
216
            && $this->getIdentity(true)->getGrantHighVolume()
217
            && $this->getIdentity(true)->getGrantCreateEditMovePage();
218
    }
219
220
    /**
221
     * @throws OAuthException
222
     * @throws CurlException
223
     * @throws OptimisticLockFailedException
224
     * @throws Exception
225
     */
226
    public function refreshIdentity()
227
    {
228
        $this->loadIdentity();
229
230
        if ($this->identity === null) {
231
            $this->identity = new OAuthIdentity();
232
            $this->identity->setUserId($this->user->getId());
233
            $this->identity->setDatabase($this->database);
234
        }
235
236
        $token = $this->loadAccessToken();
237
238
        try {
239
            $rawTicket = $this->oauthProtocolHelper->getIdentityTicket($token->getToken(), $token->getSecret());
240
        }
241
        catch (Exception $ex) {
242
            if (strpos($ex->getMessage(), "mwoauthdatastore-access-token-not-found") !== false) {
243
                throw new OAuthException('No approved grants for this access token.', -1, $ex);
244
            }
245
246
            throw $ex;
247
        }
248
249
        $this->identity->populate($rawTicket);
250
251
        if (!$this->identityIsValid()) {
252
            throw new OAuthException('Identity ticket is not valid!');
253
        }
254
255
        $this->identity->save();
256
257
        $this->user->setOnWikiName($this->identity->getUsername());
258
        $this->user->save();
259
    }
260
261
    /**
262
     * @return string
263
     * @throws CurlException
264
     */
265
    public function getRequestToken()
266
    {
267
        $token = $this->oauthProtocolHelper->getRequestToken();
268
269
        $this->partiallyLinked = true;
270
        $this->linked = false;
271
272
        $this->database
273
            ->prepare('DELETE FROM oauthtoken WHERE user = :user AND type = :type')
274
            ->execute(array(':user' => $this->user->getId(), ':type' => self::TOKEN_REQUEST));
275
276
        $this->database
277
            ->prepare('INSERT INTO oauthtoken (user, type, token, secret, expiry) VALUES (:user, :type, :token, :secret, DATE_ADD(NOW(), INTERVAL 1 DAY))')
278
            ->execute(array(
279
                ':user'   => $this->user->getId(),
280
                ':type'   => self::TOKEN_REQUEST,
281
                ':token'  => $token->key,
282
                ':secret' => $token->secret,
283
            ));
284
285
        return $this->oauthProtocolHelper->getAuthoriseUrl($token->key);
286
    }
287
288
    /**
289
     * @param $verificationToken
290
     *
291
     * @throws ApplicationLogicException
292
     * @throws CurlException
293
     * @throws OAuthException
294
     * @throws OptimisticLockFailedException
295
     */
296
    public function completeHandshake($verificationToken)
297
    {
298
        $this->getTokenStatement->execute(array(':user' => $this->user->getId(), ':type' => self::TOKEN_REQUEST));
299
300
        /** @var OAuthToken $token */
301
        $token = $this->getTokenStatement->fetchObject(OAuthToken::class);
302
        $this->getTokenStatement->closeCursor();
303
304
        if ($token === false) {
0 ignored issues
show
introduced by
The condition $token === false is always false.
Loading history...
305
            throw new ApplicationLogicException('Cannot find request token');
306
        }
307
308
        $token->setDatabase($this->database);
309
310
        $accessToken = $this->oauthProtocolHelper->callbackCompleted($token->getToken(), $token->getSecret(),
311
            $verificationToken);
312
313
        $clearStatement = $this->database->prepare('DELETE FROM oauthtoken WHERE user = :u AND type = :t');
314
        $clearStatement->execute(array(':u' => $this->user->getId(), ':t' => self::TOKEN_ACCESS));
315
316
        $token->setToken($accessToken->key);
317
        $token->setSecret($accessToken->secret);
318
        $token->setType(self::TOKEN_ACCESS);
319
        $token->setExpiry(null);
320
        $token->save();
321
322
        $this->partiallyLinked = false;
323
        $this->linked = true;
324
325
        $this->refreshIdentity();
326
    }
327
328
    public function detach()
329
    {
330
        $this->loadIdentity();
331
332
        $this->identity->delete();
333
        $statement = $this->database->prepare('DELETE FROM oauthtoken WHERE user = :user');
334
        $statement->execute(array(':user' => $this->user->getId()));
335
336
        $this->identity = null;
337
        $this->linked = false;
338
        $this->partiallyLinked = false;
339
    }
340
341
    /**
342
     * @param bool $expiredOk
343
     *
344
     * @return OAuthIdentity
345
     * @throws OAuthException
346
     */
347
    public function getIdentity($expiredOk = false)
348
    {
349
        $this->loadIdentity();
350
351
        if (!$this->identityIsValid($expiredOk)) {
352
            throw new OAuthException('Stored identity is not valid.');
353
        }
354
355
        return $this->identity;
356
    }
357
358
    public function doApiCall($params, $method)
359
    {
360
        // Ensure we're logged in
361
        $params['assert'] = 'user';
362
363
        $token = $this->loadAccessToken();
364
        return $this->oauthProtocolHelper->apiCall($params, $token->getToken(), $token->getSecret(), $method);
365
    }
366
367
    /**
368
     * @param bool $expiredOk
369
     *
370
     * @return bool
371
     */
372
    private function identityIsValid($expiredOk = false)
373
    {
374
        $this->loadIdentity();
375
376
        if ($this->identity === null) {
377
            return false;
378
        }
379
380
        if ($this->identity->getIssuedAtTime() === false
381
            || $this->identity->getExpirationTime() === false
0 ignored issues
show
introduced by
The condition $this->identity->getExpirationTime() === false is always false.
Loading history...
382
            || $this->identity->getAudience() === false
0 ignored issues
show
introduced by
The condition $this->identity->getAudience() === false is always false.
Loading history...
383
            || $this->identity->getIssuer() === false
0 ignored issues
show
introduced by
The condition $this->identity->getIssuer() === false is always false.
Loading history...
384
        ) {
385
            // this isn't populated properly.
386
            return false;
387
        }
388
389
        $issue = DateTimeImmutable::createFromFormat("U", $this->identity->getIssuedAtTime());
390
        $now = new DateTimeImmutable();
391
392
        if ($issue > $now) {
393
            // wat.
394
            return false;
395
        }
396
397
        if ($this->identityExpired() && !$expiredOk) {
398
            // soz.
399
            return false;
400
        }
401
402
        if ($this->identity->getAudience() !== $this->siteConfiguration->getOAuthConsumerToken()) {
403
            // token not issued for us
404
405
            // we allow cases where the cache is expired and the cache is for a legacy token
406
            if (!($expiredOk && in_array($this->identity->getAudience(), $this->legacyTokens))) {
407
                return false;
408
            }
409
        }
410
411
        if ($this->identity->getIssuer() !== $this->siteConfiguration->getOauthMediaWikiCanonicalServer()) {
412
            // token not issued by the right person
413
            return false;
414
        }
415
416
        // can't find a reason to not trust it
417
        return true;
418
    }
419
420
    /**
421
     * @return bool
422
     */
423
    public function identityExpired()
424
    {
425
        // allowed max age
426
        $gracePeriod = $this->siteConfiguration->getOauthIdentityGraceTime();
427
428
        $expiry = DateTimeImmutable::createFromFormat("U", $this->identity->getExpirationTime());
429
        $graceExpiry = $expiry->modify($gracePeriod);
430
        $now = new DateTimeImmutable();
431
432
        return $graceExpiry < $now;
433
    }
434
435
    /**
436
     * Loads the OAuth identity from the database for the current user.
437
     */
438
    private function loadIdentity()
439
    {
440
        if ($this->identityLoaded) {
441
            return;
442
        }
443
444
        $statement = $this->database->prepare('SELECT * FROM oauthidentity WHERE user = :user');
445
        $statement->execute(array(':user' => $this->user->getId()));
446
        /** @var OAuthIdentity $obj */
447
        $obj = $statement->fetchObject(OAuthIdentity::class);
448
449
        if ($obj === false) {
0 ignored issues
show
introduced by
The condition $obj === false is always false.
Loading history...
450
            // failed to load identity.
451
            $this->identityLoaded = true;
452
            $this->identity = null;
453
454
            return;
455
        }
456
457
        $obj->setDatabase($this->database);
458
        $this->identityLoaded = true;
459
        $this->identity = $obj;
460
    }
461
462
    /**
463
     * @return OAuthToken
464
     * @throws OAuthException
465
     */
466
    private function loadAccessToken()
467
    {
468
        if (!$this->accessTokenLoaded) {
469
            $this->getTokenStatement->execute(array(':user' => $this->user->getId(), ':type' => self::TOKEN_ACCESS));
470
            /** @var OAuthToken $token */
471
            $token = $this->getTokenStatement->fetchObject(OAuthToken::class);
472
            $this->getTokenStatement->closeCursor();
473
474
            if ($token === false) {
0 ignored issues
show
introduced by
The condition $token === false is always false.
Loading history...
475
                throw new OAuthException('Access token not found!');
476
            }
477
478
            $this->accessToken = $token;
479
            $this->accessTokenLoaded = true;
480
        }
481
482
        return $this->accessToken;
483
    }
484
}
485