Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 313
0.00% covered (danger)
0.00%
0 / 44
CRAP
0.00% covered (danger)
0.00%
0 / 1
Consumer
0.00% covered (danger)
0.00%
0 / 313
0.00% covered (danger)
0.00%
0 / 44
7140
0.00% covered (danger)
0.00%
0 / 1
 getSchema
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
2
 getFieldPermissionChecks
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getConsumerClass
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isOAuth2
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 newFromKey
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 newFromNameVersionUser
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 newGrants
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllStages
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConsumerKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCallbackUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCallbackIsPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEmailAuthenticated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeveloperAgreement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOwnerOnly
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOAuthVersion
n/a
0 / 0
n/a
0 / 0
0
 getWiki
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGrants
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRegistration
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSecretKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRsaKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRestrictions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStageTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeleted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalUserId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 generateCallbackUrl
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getCurrentAuthorization
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 authorize
n/a
0 / 0
n/a
0 / 0
0
 conductAuthorizationChecks
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
56
 saveAuthorization
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
30
 isUsableBy
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 normalizeValues
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 encodeRow
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 decodeRow
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 __get
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 userCanSee
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 userCanSeePrivate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 userCanSeeEmail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userCanSeeSecurity
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 userCanSeeSecret
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * (c) Aaron Schulz 2013, GPL
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 */
21
22namespace MediaWiki\Extension\OAuth\Backend;
23
24use FormatJson;
25use IContextSource;
26use IDBAccessObject;
27use LogicException;
28use MediaWiki\Extension\OAuth\Entity\ClientEntity as OAuth2Client;
29use MediaWiki\Linker\Linker;
30use MediaWiki\MediaWikiServices;
31use MediaWiki\SpecialPage\SpecialPage;
32use MediaWiki\User\User;
33use MediaWiki\WikiMap\WikiMap;
34use Message;
35use MWRestrictions;
36use Wikimedia\Rdbms\IDatabase;
37
38/**
39 * Representation of an OAuth consumer.
40 */
41abstract class Consumer extends MWOAuthDAO {
42    public const OAUTH_VERSION_1 = 1;
43    public const OAUTH_VERSION_2 = 2;
44
45    /** @var array Backwards-compatibility grant mappings */
46    public static $mapBackCompatGrants = [
47        'useoauth' => 'basic',
48        'authonly' => 'mwoauth-authonly',
49        'authonlyprivate' => 'mwoauth-authonlyprivate',
50    ];
51
52    /** @var int|null Unique ID */
53    protected $id;
54    /** @var string Hex token */
55    protected $consumerKey;
56    /** @var string Name of connected application */
57    protected $name;
58    /** @var int Publisher's central user ID. $wgMWOAuthSharedUserIDs defines which central ID
59     *    provider to use.
60     */
61    protected $userId;
62    /** @var string Version used for handshake breaking changes */
63    protected $version;
64    /** @var string OAuth callback URL for authorization step */
65    protected $callbackUrl;
66    /**
67     * @var bool OAuth callback URL is a prefix and we allow all URLs which
68     *   have callbackUrl as the prefix
69     */
70    protected $callbackIsPrefix;
71    /** @var string Application description */
72    protected $description;
73    /** @var string Publisher email address */
74    protected $email;
75    /** @var string|null TS_MW timestamp of when email address was confirmed */
76    protected $emailAuthenticated;
77    /** @var bool User accepted the developer agreement */
78    protected $developerAgreement;
79    /** @var bool Consumer is for use by the owner only */
80    protected $ownerOnly;
81    /** @var int Version of the OAuth protocol */
82    protected $oauthVersion;
83    /** @var string Wiki ID the application can be used on (or "*" for all) */
84    protected $wiki;
85    /** @var string TS_MW timestamp of proposal */
86    protected $registration;
87    /** @var string Secret HMAC key */
88    protected $secretKey;
89    /** @var string Public RSA key */
90    protected $rsaKey;
91    /** @var array List of grants */
92    protected $grants;
93    /** @var MWRestrictions IP restrictions */
94    protected $restrictions;
95    /** @var int MWOAuthConsumer::STAGE_* constant */
96    protected $stage;
97    /** @var string TS_MW timestamp of last stage change */
98    protected $stageTimestamp;
99    /** @var bool Indicates this consumer's information is suppressed */
100    protected $deleted;
101    /** @var bool Indicates whether the client (consumer) is able to keep the secret */
102    protected $oauth2IsConfidential;
103    /** @var array OAuth2 grant types available to the client */
104    protected $oauth2GrantTypes;
105
106    /* Stages that registered consumer takes (stored in DB) */
107    public const STAGE_PROPOSED = 0;
108    public const STAGE_APPROVED = 1;
109    public const STAGE_REJECTED = 2;
110    public const STAGE_EXPIRED  = 3;
111    public const STAGE_DISABLED = 4;
112
113    /** @var int|false|null Cache for local ID looked up from $userId */
114    protected $localUserId;
115
116    /**
117     * Maps stage ids to human-readable names which describe them as a state
118     * @var array<int,string>
119     */
120    public static $stageNames = [
121        self::STAGE_PROPOSED => 'proposed',
122        self::STAGE_REJECTED => 'rejected',
123        self::STAGE_EXPIRED  => 'expired',
124        self::STAGE_APPROVED => 'approved',
125        self::STAGE_DISABLED => 'disabled',
126    ];
127
128    /**
129     * Maps stage ids to human-readable names which describe them as an action (which would result
130     * in that stage)
131     * @var array<int,string>
132     */
133    public static $stageActionNames = [
134        self::STAGE_PROPOSED => 'propose',
135        self::STAGE_REJECTED => 'reject',
136        self::STAGE_EXPIRED  => 'propose',
137        self::STAGE_APPROVED => 'approve',
138        self::STAGE_DISABLED => 'disable',
139    ];
140
141    /**
142     * Get member => db field mapping
143     * Loads all fields to avoid unnecessary querying
144     *
145     * @return array
146     */
147    protected static function getSchema() {
148        return [
149            'table'          => 'oauth_registered_consumer',
150            'fieldColumnMap' => [
151                'id'                    => 'oarc_id',
152                'consumerKey'           => 'oarc_consumer_key',
153                'name'                  => 'oarc_name',
154                'userId'                => 'oarc_user_id',
155                'version'               => 'oarc_version',
156                'callbackUrl'           => 'oarc_callback_url',
157                'callbackIsPrefix'      => 'oarc_callback_is_prefix',
158                'description'           => 'oarc_description',
159                'email'                 => 'oarc_email',
160                'emailAuthenticated'    => 'oarc_email_authenticated',
161                'oauthVersion'          => 'oarc_oauth_version',
162                'developerAgreement'    => 'oarc_developer_agreement',
163                'ownerOnly'             => 'oarc_owner_only',
164                'wiki'                  => 'oarc_wiki',
165                'grants'                => 'oarc_grants',
166                'registration'          => 'oarc_registration',
167                'secretKey'             => 'oarc_secret_key',
168                'rsaKey'                => 'oarc_rsa_key',
169                'restrictions'          => 'oarc_restrictions',
170                'stage'                 => 'oarc_stage',
171                'stageTimestamp'        => 'oarc_stage_timestamp',
172                'deleted'               => 'oarc_deleted',
173                'oauth2IsConfidential'  => 'oarc_oauth2_is_confidential',
174                'oauth2GrantTypes'      => 'oarc_oauth2_allowed_grants',
175            ],
176            'idField'        => 'id',
177            'autoIncrField'  => 'id',
178        ];
179    }
180
181    protected static function getFieldPermissionChecks() {
182        return [
183            'name'             => 'userCanSee',
184            'userId'           => 'userCanSee',
185            'version'          => 'userCanSee',
186            'callbackUrl'      => 'userCanSee',
187            'callbackIsPrefix' => 'userCanSee',
188            'description'      => 'userCanSee',
189            'rsaKey'           => 'userCanSee',
190            'email'            => 'userCanSeeEmail',
191            'secretKey'        => 'userCanSeeSecret',
192            'restrictions'     => 'userCanSeeSecurity',
193        ];
194    }
195
196    /**
197     * @param array $data
198     * @return string
199     */
200    protected static function getConsumerClass( array $data ) {
201        return static::isOAuth2( $data ) ?
202            OAuth2Client::class :
203            OAuth1Consumer::class;
204    }
205
206    /**
207     * @param array $data
208     * @return bool
209     */
210    protected static function isOAuth2( array $data = [] ) {
211        $oauthVersion = $data['oarc_oauth_version'] ?? $data['oauthVersion'];
212        return (int)$oauthVersion === self::OAUTH_VERSION_2;
213    }
214
215    /**
216     * @param IDatabase $db
217     * @param string|null $key
218     * @param int $flags IDBAccessObject::READ_* bitfield
219     * @return Consumer|false
220     */
221    public static function newFromKey( IDatabase $db, $key, $flags = 0 ) {
222        $row = $db->selectRow( static::getTable(),
223            array_values( static::getFieldColumnMap() ),
224            [ 'oarc_consumer_key' => (string)$key ],
225            __METHOD__,
226            ( $flags & IDBAccessObject::READ_LOCKING ) ? [ 'FOR UPDATE' ] : []
227        );
228
229        if ( $row ) {
230            return static::newFromRow( $db, $row );
231        } else {
232            return false;
233        }
234    }
235
236    /**
237     * @param IDatabase $db
238     * @param string $name
239     * @param string $version
240     * @param int $userId Central user ID
241     * @param int $flags IDBAccessObject::READ_* bitfield
242     * @return Consumer|bool
243     */
244    public static function newFromNameVersionUser(
245        IDatabase $db, $name, $version, $userId, $flags = 0
246    ) {
247        $row = $db->selectRow( static::getTable(),
248            array_values( static::getFieldColumnMap() ),
249            [
250                'oarc_name' => (string)$name,
251                'oarc_version' => (string)$version,
252                'oarc_user_id' => (int)$userId
253            ],
254            __METHOD__,
255            ( $flags & IDBAccessObject::READ_LOCKING ) ? [ 'FOR UPDATE' ] : []
256        );
257
258        if ( $row ) {
259            return static::newFromRow( $db, $row );
260        } else {
261            return false;
262        }
263    }
264
265    /**
266     * @return string[]
267     */
268    public static function newGrants() {
269        return [];
270    }
271
272    /**
273     * @return int[]
274     */
275    public static function getAllStages() {
276        return [
277            self::STAGE_PROPOSED,
278            self::STAGE_REJECTED,
279            self::STAGE_EXPIRED,
280            self::STAGE_APPROVED,
281            self::STAGE_DISABLED,
282        ];
283    }
284
285    /**
286     * Internal ID (DB primary key).
287     * @return int
288     */
289    public function getId() {
290        return $this->get( 'id' );
291    }
292
293    /**
294     * Consumer key (32-character hexadecimal string that's used in the OAuth protocol
295     * and in URLs). This is used as the consumer ID for most external purposes.
296     * @return string
297     */
298    public function getConsumerKey() {
299        return $this->get( 'consumerKey' );
300    }
301
302    /**
303     * Name of the consumer.
304     * @return string
305     */
306    public function getName() {
307        return $this->get( 'name' );
308    }
309
310    /**
311     * Central ID of the owner.
312     * @return int
313     */
314    public function getUserId() {
315        return $this->get( 'userId' );
316    }
317
318    /**
319     * Consumer version. This is mostly meant for humans: different versions of the same
320     * application have different keys and are handled as different consumers internally.
321     * @return string
322     */
323    public function getVersion() {
324        return $this->get( 'version' );
325    }
326
327    /**
328     * Callback URL (or prefix). The browser will be redirected to this URL at the end of
329     * an OAuth handshake. See getCallbackIsPrefix() for the interpretation of this field.
330     * @return string
331     */
332    public function getCallbackUrl() {
333        return $this->get( 'callbackUrl' );
334    }
335
336    /**
337     * When false, the callback URL will be determined by getCallbackUrl(). When true,
338     * getCallbackUrl() returns a prefix; the callback URL must be provided by the caller
339     * and must match the prefix. For the exact definition of "match", see
340     * MWOAuthServer::checkCallback().
341     * @return bool
342     */
343    public function getCallbackIsPrefix() {
344        return $this->get( 'callbackIsPrefix' );
345    }
346
347    /**
348     * Description of the consumer. Currently interpreted as plain text; might change to wikitext
349     * in the future.
350     * @return string
351     */
352    public function getDescription() {
353        return $this->get( 'description' );
354    }
355
356    /**
357     * Email address of the owner.
358     * @return string
359     */
360    public function getEmail() {
361        return $this->get( 'email' );
362    }
363
364    /**
365     * Date of verifying the email, in TS_MW format. In practice this will be the same as
366     * getRegistration().
367     * @return string|null
368     */
369    public function getEmailAuthenticated() {
370        return $this->get( 'emailAuthenticated' );
371    }
372
373    /**
374     * Did the user accept the developer agreement (the terms of use checkbox at the bottom of the
375     * registration form)? Except for very old users, always true.
376     * @return bool
377     */
378    public function getDeveloperAgreement() {
379        return $this->get( 'developerAgreement' );
380    }
381
382    /**
383     * Owner-only consumers will use one-legged flow instead of three-legged (see
384     * https://github.com/Mashape/mashape-oauth/blob/master/FLOWS.md#oauth-10a-one-legged ); there
385     * is only one user (who is the same as the owner) and they learn the access token at
386     * consumer registration time.
387     * @return bool
388     */
389    public function getOwnerOnly() {
390        return $this->get( 'ownerOnly' );
391    }
392
393    /**
394     * @return int
395     */
396    abstract public function getOAuthVersion();
397
398    /**
399     * The wiki on which the consumer is allowed to access user accounts. A wiki ID or '*' for all.
400     * @return string
401     */
402    public function getWiki() {
403        return $this->get( 'wiki' );
404    }
405
406    /**
407     * The list of grants required by this application.
408     * @return string[]
409     */
410    public function getGrants() {
411        return $this->get( 'grants' );
412    }
413
414    /**
415     * Consumer registration date in TS_MW format.
416     * @return string
417     */
418    public function getRegistration() {
419        return $this->get( 'registration' );
420    }
421
422    /**
423     * Secret key used to derive the consumer secret for HMAC-SHA1 signed OAuth requests.
424     * The actual consumer secret will be calculated via Utils::hmacDBSecret() to mitigate
425     * DB leaks.
426     * @return string
427     */
428    public function getSecretKey() {
429        return $this->get( 'secretKey' );
430    }
431
432    /**
433     * Public RSA key for RSA-SHA1 signed OAuth requests.
434     * @return string
435     */
436    public function getRsaKey() {
437        return $this->get( 'rsaKey' );
438    }
439
440    /**
441     * Application restrictions (such as allowed IPs).
442     * @return MWRestrictions
443     */
444    public function getRestrictions() {
445        return $this->get( 'restrictions' );
446    }
447
448    /**
449     * Stage at which the consumer is in the review workflow (proposed, approved etc).
450     * @return int One of the STAGE_* constants
451     */
452    public function getStage() {
453        return $this->get( 'stage' );
454    }
455
456    /**
457     * Date at which the consumer was moved to the current stage, in TS_MW format.
458     * @return string
459     */
460    public function getStageTimestamp() {
461        return $this->get( 'stageTimestamp' );
462    }
463
464    /**
465     * Is the consumer suppressed? (There is no plain deletion; the closest equivalent is the
466     * rejected/disabled stage.)
467     * @return bool
468     */
469    public function getDeleted() {
470        return $this->get( 'deleted' );
471    }
472
473    /**
474     * Local ID of the owner (or false if there is no local account).
475     * @return int|false
476     */
477    public function getLocalUserId() {
478        if ( $this->localUserId === null ) {
479            $user = Utils::getLocalUserFromCentralId( $this->getUserId() );
480            if ( $user ) {
481                $this->localUserId = $user->getId();
482            } else {
483                $this->localUserId = false;
484            }
485        }
486        return $this->localUserId;
487    }
488
489    /**
490     * @param MWOAuthDataStore $dataStore
491     * @param string $verifyCode verification code
492     * @param string $requestKey original request key from /initiate
493     * @return string the url for redirection
494     */
495    public function generateCallbackUrl( $dataStore, $verifyCode, $requestKey ) {
496        $callback = $dataStore->getCallbackUrl( $this->key, $requestKey );
497
498        if ( $callback === 'oob' ) {
499            $callback = $this->getCallbackUrl();
500        }
501
502        return wfAppendQuery( $callback, [
503            'oauth_verifier' => $verifyCode,
504            'oauth_token'    => $requestKey
505        ] );
506    }
507
508    /**
509     * Attempts to find an authorization by this user for this consumer. Since a user can
510     * accept a consumer multiple times (once for "*" and once for each specific wiki),
511     * there can several access tokens per-wiki (with varying grants) for a consumer.
512     * This will choose the most wiki-specific access token. The precedence is:
513     * a) The acceptance for wiki X if the consumer is applicable only to wiki X
514     * b) The acceptance for wiki $wikiId (if the consumer is applicable to it)
515     * c) The acceptance for wikis "*" (all wikis)
516     *
517     * Users might want more grants on some wikis than on "*". Note that the reverse would not
518     * make sense, since the consumer could just use the "*" acceptance if it has more grants.
519     *
520     * @param User $mwUser (local wiki user) User who may or may not have authorizations
521     * @param string $wikiId
522     * @throws MWOAuthException
523     * @return ConsumerAcceptance|bool
524     */
525    public function getCurrentAuthorization( User $mwUser, $wikiId ) {
526        $dbr = Utils::getCentralDB( DB_REPLICA );
527
528        $centralUserId = Utils::getCentralIdFromLocalUser( $mwUser );
529        if ( !$centralUserId ) {
530            throw new MWOAuthException(
531                'mwoauthserver-invalid-user',
532                [
533                    'consumer_name' => $this->getName(),
534                    Message::rawParam( Linker::makeExternalLink(
535                        'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E008',
536                        'E008',
537                        true
538                    ) ),
539                    'consumer' => $this->getConsumerKey(),
540                ]
541            );
542        }
543
544        $checkWiki = $this->getWiki() !== '*' ? $this->getWiki() : $wikiId;
545
546        $cmra = ConsumerAcceptance::newFromUserConsumerWiki(
547            $dbr,
548            $centralUserId,
549            $this,
550            $checkWiki,
551            0,
552            $this->getOAuthVersion()
553        );
554        if ( !$cmra ) {
555            $cmra = ConsumerAcceptance::newFromUserConsumerWiki(
556                $dbr,
557                $centralUserId,
558                $this,
559                '*',
560                0,
561                $this->getOAuthVersion()
562            );
563        }
564        return $cmra;
565    }
566
567    /**
568     * @param User $mwUser
569     * @param bool $update
570     * @param array $grants
571     * @param string|null $requestTokenKey
572     * @return mixed
573     */
574    abstract public function authorize( User $mwUser, $update, $grants, $requestTokenKey = null );
575
576    /**
577     * Verify that this user can authorize this consumer
578     *
579     * @param User $mwUser
580     * @throws MWOAuthException
581     */
582    protected function conductAuthorizationChecks( User $mwUser ) {
583        global $wgBlockDisablesLogin;
584
585        // Check that user and consumer are in good standing
586        if ( $mwUser->isLocked() || ( $wgBlockDisablesLogin && $mwUser->getBlock() ) ) {
587            throw new MWOAuthException( 'mwoauthserver-insufficient-rights', [
588                Message::rawParam( Linker::makeExternalLink(
589                    'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E007',
590                    'E007',
591                    true
592                ) ),
593                'consumer' => $this->getConsumerKey(),
594                'consumer_name' => $this->getName(),
595            ] );
596        }
597
598        if ( $this->getDeleted() ) {
599            throw new MWOAuthException( 'mwoauthserver-bad-consumer-key', [
600                Message::rawParam( Linker::makeExternalLink(
601                    'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E006',
602                    'E006',
603                    true
604                ) ),
605                'consumer' => $this->getConsumerKey(),
606                'consumer_name' => $this->getName(),
607            ] );
608        } elseif ( !$this->isUsableBy( $mwUser ) ) {
609            $owner = Utils::getCentralUserNameFromId(
610                $this->getUserId(),
611                $mwUser
612            );
613            throw new MWOAuthException(
614                'mwoauthserver-bad-consumer',
615                [
616                    'consumer_name' => $this->getName(),
617                    'talkpage' => Utils::getCentralUserTalk( $owner ),
618                    Message::rawParam( Linker::makeExternalLink(
619                        'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E005',
620                        'E005',
621                        true
622                    ) ),
623                    'consumer' => $this->getConsumerKey(),
624                ]
625            );
626        } elseif ( $this->getOwnerOnly() ) {
627            throw new MWOAuthException( 'mwoauthserver-consumer-owner-only', [
628                'consumer_name' => $this->getName(),
629                SpecialPage::getTitleFor(
630                    'OAuthConsumerRegistration', 'update/' . $this->getConsumerKey()
631                ),
632                Message::rawParam( Linker::makeExternalLink(
633                    'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E010',
634                    'E010',
635                    true
636                ) ),
637                'consumer' => $this->getConsumerKey(),
638            ] );
639        }
640    }
641
642    /**
643     * @param User $mwUser
644     * @param bool $update
645     * @param string[] $grants
646     * @return ConsumerAcceptance
647     * @throws MWOAuthException
648     */
649    protected function saveAuthorization( User $mwUser, $update, $grants ) {
650        // CentralAuth may abort here if there is no global account for this user
651        $centralUserId = Utils::getCentralIdFromLocalUser( $mwUser );
652        if ( !$centralUserId ) {
653            throw new MWOAuthException(
654                'mwoauthserver-invalid-user',
655                [
656                    'consumer_name' => $this->getName(),
657                    Message::rawParam( Linker::makeExternalLink(
658                        'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E008',
659                        'E008',
660                        true
661                    ) ),
662                    'consumer' => $this->getConsumerKey(),
663                ]
664            );
665        }
666
667        $dbw = Utils::getCentralDB( DB_PRIMARY );
668        // Check if this authorization exists
669        $cmra = $this->getCurrentAuthorization( $mwUser, WikiMap::getCurrentWikiId() );
670
671        if ( $update ) {
672            // This should be an update to an existing authorization
673            if ( !$cmra ) {
674                // update requested, but no existing key
675                throw new MWOAuthException( 'mwoauthserver-invalid-request', [
676                    'consumer' => $this->getConsumerKey(),
677                    'consumer_name' => $this->getName(),
678                ] );
679            }
680            $cmra->setFields( [
681                'wiki'   => $this->getWiki(),
682                'grants' => $grants
683            ] );
684            $cmra->save( $dbw );
685        } elseif ( !$cmra ) {
686            // Add the Authorization to the database
687            $accessToken = MWOAuthDataStore::newToken();
688            $cmra = ConsumerAcceptance::newFromArray( [
689                'id'           => null,
690                'wiki'         => $this->getWiki(),
691                'userId'       => $centralUserId,
692                'consumerId'   => $this->getId(),
693                'accessToken'  => $accessToken->key,
694                'accessSecret' => $accessToken->secret,
695                'grants'       => $grants,
696                'accepted'     => wfTimestampNow(),
697                'oauth_version' => $this->getOAuthVersion()
698            ] );
699            $cmra->save( $dbw );
700        }
701
702        return $cmra;
703    }
704
705    /**
706     * Check if the consumer is usable by $user
707     *
708     * "Usable by $user" includes:
709     * - Approved for multi-user use
710     * - Approved for owner-only use and is owned by $user
711     * - Still pending approval and is owned by $user
712     *
713     * @param User $user
714     * @return bool
715     */
716    public function isUsableBy( User $user ) {
717        if ( $this->stage === self::STAGE_APPROVED && !$this->getOwnerOnly() ) {
718            return true;
719        } elseif ( $this->stage === self::STAGE_PROPOSED || $this->stage === self::STAGE_APPROVED ) {
720            $centralId = Utils::getCentralIdFromLocalUser( $user );
721            return ( $centralId && $this->userId === $centralId );
722        }
723
724        return false;
725    }
726
727    protected function normalizeValues() {
728        // Keep null values since we're constructing w/ them to auto-increment
729        $this->id = $this->id === null ? null : (int)$this->id;
730        $this->userId = (int)$this->userId;
731        $this->registration = wfTimestamp( TS_MW, $this->registration );
732        $this->stage = (int)$this->stage;
733        $this->stageTimestamp = wfTimestamp( TS_MW, $this->stageTimestamp );
734        $this->emailAuthenticated = wfTimestampOrNull( TS_MW, $this->emailAuthenticated );
735        $this->grants = (array)$this->grants;
736        $this->callbackIsPrefix = (bool)$this->callbackIsPrefix;
737        $this->ownerOnly = (bool)$this->ownerOnly;
738        $this->oauthVersion = (int)$this->oauthVersion;
739        $this->developerAgreement = (bool)$this->developerAgreement;
740        $this->deleted = (bool)$this->deleted;
741        $this->oauth2IsConfidential = (bool)$this->oauth2IsConfidential;
742    }
743
744    protected function encodeRow( IDatabase $db, $row ) {
745        // For compatibility with other wikis in the farm, un-remap some grants
746        foreach ( self::$mapBackCompatGrants as $old => $new ) {
747            while ( ( $i = array_search( $new, $row['oarc_grants'], true ) ) !== false ) {
748                $row['oarc_grants'][$i] = $old;
749            }
750        }
751
752        $row['oarc_registration'] = $db->timestamp( $row['oarc_registration'] );
753        $row['oarc_stage_timestamp'] = $db->timestamp( $row['oarc_stage_timestamp'] );
754        $row['oarc_restrictions'] = $row['oarc_restrictions']->toJson();
755        $row['oarc_grants'] = FormatJson::encode( $row['oarc_grants'] );
756        $row['oarc_email_authenticated'] =
757            $db->timestampOrNull( $row['oarc_email_authenticated'] );
758        $row['oarc_oauth2_allowed_grants'] = FormatJson::encode(
759            $row['oarc_oauth2_allowed_grants']
760        );
761        return $row;
762    }
763
764    protected function decodeRow( IDatabase $db, $row ) {
765        $row['oarc_registration'] = wfTimestamp( TS_MW, $row['oarc_registration'] );
766        $row['oarc_stage'] = (int)$row['oarc_stage'];
767        $row['oarc_stage_timestamp'] = wfTimestamp( TS_MW, $row['oarc_stage_timestamp'] );
768        $row['oarc_restrictions'] = MWRestrictions::newFromJson( $row['oarc_restrictions'] );
769        $row['oarc_grants'] = FormatJson::decode( $row['oarc_grants'], true );
770        $row['oarc_user_id'] = (int)$row['oarc_user_id'];
771        $row['oarc_email_authenticated'] =
772            wfTimestampOrNull( TS_MW, $row['oarc_email_authenticated'] );
773        $row['oarc_oauth2_allowed_grants'] = FormatJson::decode(
774            $row['oarc_oauth2_allowed_grants'] ?? 'null', true
775        );
776
777        // For backwards compatibility, remap some grants
778        foreach ( self::$mapBackCompatGrants as $old => $new ) {
779            while ( ( $i = array_search( $old, $row['oarc_grants'], true ) ) !== false ) {
780                $row['oarc_grants'][$i] = $new;
781            }
782        }
783
784        return $row;
785    }
786
787    /**
788     * Magic method so that fields like $consumer->secret and $consumer->key work.
789     * This allows MWOAuthConsumer to be a replacement for OAuthConsumer
790     * in lib/OAuth.php without inheriting.
791     * @param mixed $prop
792     * @return mixed
793     */
794    public function __get( $prop ) {
795        if ( $prop === 'key' ) {
796            return $this->consumerKey;
797        } elseif ( $prop === 'secret' ) {
798            return Utils::hmacDBSecret( $this->secretKey );
799        } elseif ( $prop === 'callback_url' ) {
800            return $this->callbackUrl;
801        } else {
802            throw new LogicException( 'Direct property access attempt: ' . $prop );
803        }
804    }
805
806    /**
807     * Can the user see a field with "standard" visibility?
808     * @param string $name Field name
809     * @param IContextSource $context
810     * @return true|Message True if allowed, error message otherwise.
811     */
812    protected function userCanSee( $name, IContextSource $context ) {
813        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
814
815        if ( $this->getDeleted()
816            && !$permissionManager->userHasRight( $context->getUser(), 'mwoauthviewsuppressed' )
817        ) {
818            return $context->msg( 'mwoauth-field-hidden' );
819        } else {
820            return true;
821        }
822    }
823
824    /**
825     * Can the user see a private field?
826     * @param string $name Field name
827     * @param IContextSource $context
828     * @return true|Message True if allowed, error message otherwise.
829     */
830    protected function userCanSeePrivate( $name, IContextSource $context ) {
831        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
832
833        if ( !$permissionManager->userHasRight( $context->getUser(), 'mwoauthviewprivate' ) ) {
834            return $context->msg( 'mwoauth-field-private' );
835        } else {
836            return $this->userCanSee( $name, $context );
837        }
838    }
839
840    /**
841     * Can the user see the app owner's email?
842     * @param string $name Field name
843     * @param IContextSource $context
844     * @return true|Message True if allowed, error message otherwise.
845     */
846    protected function userCanSeeEmail( $name, IContextSource $context ) {
847        // although email is not a security-related field, it's handled the same way
848        return $this->userCanSeeSecurity( $name, $context );
849    }
850
851    /**
852     * Can the user see a field that relates to how the app's owner manages application
853     * security?
854     * @param string $name Field name
855     * @param IContextSource $context
856     * @return true|Message True if allowed, error message otherwise.
857     */
858    protected function userCanSeeSecurity( $name, IContextSource $context ) {
859        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
860        $user = $context->getUser();
861
862        if ( $user->getId() === $this->getLocalUserId() ) {
863            // owners can always see the details of their apps, unless the app got deleted-suppressed
864            return $this->userCanSee( $name, $context );
865        } elseif ( $this->getOwnerOnly() ) {
866            // owner-only apps are essentially personal API tokens, nobody else's business
867            return $context->msg( 'mwoauth-field-private' );
868        } elseif ( !$permissionManager->userHasRight( $user, 'mwoauthmanageconsumer' ) ) {
869            // if you are not the owner or an admin you definitely shouldn't see security details
870            return $context->msg( 'mwoauth-field-private' );
871        } else {
872            // OAuth admin looking at non-owner-only app. Just need to check  suppression.
873            return $this->userCanSee( $name, $context );
874        }
875    }
876
877    /**
878     * Can the user see a given field containing credentials? (No.)
879     * @param string $name Field name
880     * @param IContextSource $context
881     * @return true|Message True if allowed, error message otherwise.
882     */
883    protected function userCanSeeSecret( $name, IContextSource $context ) {
884        return $context->msg( 'mwoauth-field-private' );
885    }
886}