Failed Conditions
Push — newinternal-releasecandidate ( 327c61...a30d14 )
by Simon
15:28 queued 05:26
created

OAuthUserHelper::doApiCall()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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