Test Failed
Push — newinternal ( 8c4587...b2f220 )
by Michael
15:41 queued 06:17
created

OAuthUserHelper   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 436
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 55
eloc 173
c 2
b 0
f 0
dl 0
loc 436
rs 6

20 Methods

Rating   Name   Duplication   Size   Complexity  
A completeHandshake() 0 30 2
A userIsFullyLinked() 0 7 3
A canCreateAccount() 0 5 4
A __construct() 0 16 1
A canWelcome() 0 5 4
B identityIsValid() 0 42 11
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 24 3
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 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\CurlException;
18
use Waca\Exceptions\OAuthException;
19
use Waca\Exceptions\OptimisticLockFailedException;
20
use Waca\Helpers\Interfaces\IMediaWikiClient;
21
use Waca\Helpers\Interfaces\IOAuthProtocolHelper;
22
use Waca\PdoDatabase;
23
use Waca\SiteConfiguration;
0 ignored issues
show
Bug introduced by
The type Waca\SiteConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
25
class OAuthUserHelper implements IMediaWikiClient
26
{
27
    const TOKEN_REQUEST = 'request';
28
    const TOKEN_ACCESS = 'access';
29
    /** @var PDOStatement */
30
    private static $tokenCountStatement = null;
31
    /** @var PDOStatement */
32
    private $getTokenStatement;
33
    /**
34
     * @var User
35
     */
36
    private $user;
37
    /**
38
     * @var PdoDatabase
39
     */
40
    private $database;
41
    /**
42
     * @var IOAuthProtocolHelper
43
     */
44
    private $oauthProtocolHelper;
45
    /**
46
     * @var bool|null Is the user linked to OAuth
47
     */
48
    private $linked;
49
    private $partiallyLinked;
50
    /** @var OAuthToken */
51
    private $accessToken;
52
    /** @var bool */
53
    private $accessTokenLoaded = false;
54
    /**
55
     * @var OAuthIdentity
56
     */
57
    private $identity = null;
58
    /**
59
     * @var bool
60
     */
61
    private $identityLoaded = false;
62
    /**
63
     * @var SiteConfiguration
64
     */
65
    private $siteConfiguration;
66
67
    #region Static methods
68
    public static function findUserByRequestToken($requestToken, PdoDatabase $database)
69
    {
70
        $statement = $database->prepare(<<<'SQL'
71
            SELECT u.* FROM user u 
72
            INNER JOIN oauthtoken t ON t.user = u.id 
73
            WHERE t.type = :type AND t.token = :token
74
SQL
75
        );
76
        $statement->execute(array(':type' => self::TOKEN_REQUEST, ':token' => $requestToken));
77
78
        /** @var User $user */
79
        $user = $statement->fetchObject(User::class);
80
        $statement->closeCursor();
81
82
        if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
83
            throw new ApplicationLogicException('Token not found in store, please try again');
84
        }
85
86
        $user->setDatabase($database);
87
88
        return $user;
89
    }
90
91
    public static function userIsFullyLinked(User $user, PdoDatabase $database = null)
92
    {
93
        if (self::$tokenCountStatement === null && $database === null) {
94
            throw new ApplicationLogicException('Static link request without initialised statement');
95
        }
96
97
        return self::runTokenCount($user->getId(), $database, self::TOKEN_ACCESS);
98
    }
99
100
    public static function userIsPartiallyLinked(User $user, PdoDatabase $database = null)
101
    {
102
        if (self::$tokenCountStatement === null && $database === null) {
103
            throw new ApplicationLogicException('Static link request without initialised statement');
104
        }
105
106
        if (self::userIsFullyLinked($user, $database)) {
107
            return false;
108
        }
109
110
        return self::runTokenCount($user->getId(), $database, self::TOKEN_REQUEST)
111
            || $user->getOnWikiName() == null;
112
    }
113
114
    /**
115
     * @param PdoDatabase $database
116
     */
117
    public static function prepareTokenCountStatement(PdoDatabase $database)
118
    {
119
        if (self::$tokenCountStatement === null) {
120
            self::$tokenCountStatement = $database->prepare('SELECT COUNT(*) FROM oauthtoken WHERE user = :user AND type = :type');
0 ignored issues
show
Documentation Bug introduced by
It seems like $database->prepare('SELE...user AND type = :type') can also be of type boolean. However, the property $tokenCountStatement is declared as type PDOStatement. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
121
        }
122
    }
123
124
    private static function runTokenCount($userId, $database, $tokenType)
125
    {
126
        if (self::$tokenCountStatement === null) {
127
            self::prepareTokenCountStatement($database);
128
        }
129
130
        self::$tokenCountStatement->execute(array(
131
            ':user' => $userId,
132
            ':type' => $tokenType,
133
        ));
134
135
        $tokenCount = self::$tokenCountStatement->fetchColumn();
136
        $linked = $tokenCount > 0;
137
        self::$tokenCountStatement->closeCursor();
138
139
        return $linked;
140
    }
141
142
    #endregion Static methods
143
144
    /**
145
     * OAuthUserHelper constructor.
146
     *
147
     * @param User                 $user
148
     * @param PdoDatabase          $database
149
     * @param IOAuthProtocolHelper $oauthProtocolHelper
150
     * @param SiteConfiguration    $siteConfiguration
151
     */
152
    public function __construct(
153
        User $user,
154
        PdoDatabase $database,
155
        IOAuthProtocolHelper $oauthProtocolHelper,
156
        SiteConfiguration $siteConfiguration
157
    ) {
158
        $this->user = $user;
159
        $this->database = $database;
160
        $this->oauthProtocolHelper = $oauthProtocolHelper;
161
162
        $this->linked = null;
163
        $this->partiallyLinked = null;
164
        $this->siteConfiguration = $siteConfiguration;
165
166
        self::prepareTokenCountStatement($database);
167
        $this->getTokenStatement = $this->database->prepare('SELECT * FROM oauthtoken WHERE user = :user AND type = :type');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->database->prepare...user AND type = :type') can also be of type boolean. However, the property $getTokenStatement is declared as type PDOStatement. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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