summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'OAuth/src/Backend/Consumer.php')
-rw-r--r--OAuth/src/Backend/Consumer.php800
1 files changed, 800 insertions, 0 deletions
diff --git a/OAuth/src/Backend/Consumer.php b/OAuth/src/Backend/Consumer.php
new file mode 100644
index 00000000..071ca0ce
--- /dev/null
+++ b/OAuth/src/Backend/Consumer.php
@@ -0,0 +1,800 @@
+<?php
+
+/**
+ * (c) Aaron Schulz 2013, GPL
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+namespace MediaWiki\Extensions\OAuth\Backend;
+
+use FormatJson;
+use MediaWiki\Extensions\OAuth\Entity\ClientEntity as OAuth2Client;
+use MWException;
+use User;
+use Wikimedia\Rdbms\DBConnRef;
+
+/**
+ * Representation of an OAuth consumer.
+ */
+abstract class Consumer extends MWOAuthDAO {
+ const OAUTH_VERSION_1 = 1;
+ const OAUTH_VERSION_2 = 2;
+
+ /** @var array Backwards-compatibility grant mappings */
+ public static $mapBackCompatGrants = [
+ 'useoauth' => 'basic',
+ 'authonly' => 'mwoauth-authonly',
+ 'authonlyprivate' => 'mwoauth-authonlyprivate',
+ ];
+
+ /** @var int Unique ID */
+ protected $id;
+ /** @var string Hex token */
+ protected $consumerKey;
+ /** @var string Name of connected application */
+ protected $name;
+ /** @var int Publisher's central user ID. $wgMWOAuthSharedUserIDs defines which central ID
+ * provider to use.
+ */
+ protected $userId;
+ /** @var string Version used for handshake breaking changes */
+ protected $version;
+ /** @var string OAuth callback URL for authorization step */
+ protected $callbackUrl;
+ /**
+ * @var int OAuth callback URL is a prefix and we allow all URLs which
+ * have callbackUrl as the prefix
+ */
+ protected $callbackIsPrefix;
+ /** @var string Application description */
+ protected $description;
+ /** @var string Publisher email address */
+ protected $email;
+ /** @var string TS_MW timestamp of when email address was confirmed */
+ protected $emailAuthenticated;
+ /** @var int User accepted the developer agreement */
+ protected $developerAgreement;
+ /** @var int Consumer is for use by the owner only */
+ protected $ownerOnly;
+ /** @var string Version of the OAuth protocol */
+ protected $oauthVersion;
+ /** @var string Wiki ID the application can be used on (or "*" for all) */
+ protected $wiki;
+ /** @var string TS_MW timestamp of proposal */
+ protected $registration;
+ /** @var string Secret HMAC key */
+ protected $secretKey;
+ /** @var string Public RSA key */
+ protected $rsaKey;
+ /** @var array List of grants */
+ protected $grants;
+ /** @var \MWRestrictions IP restrictions */
+ protected $restrictions;
+ /** @var int MWOAuthConsumer::STAGE_* constant */
+ protected $stage;
+ /** @var string TS_MW timestamp of last stage change */
+ protected $stageTimestamp;
+ /** @var int Indicates (if non-zero) this consumer's information is suppressed */
+ protected $deleted;
+ /** @var bool Indicates whether the client (consumer) is able to keep the secret */
+ protected $oauth2IsConfidential;
+ /** @var array OAuth2 grant types available to the client */
+ protected $oauth2GrantTypes;
+
+ /* Stages that registered consumer takes (stored in DB) */
+ const STAGE_PROPOSED = 0;
+ const STAGE_APPROVED = 1;
+ const STAGE_REJECTED = 2;
+ const STAGE_EXPIRED = 3;
+ const STAGE_DISABLED = 4;
+
+ /**
+ * Maps stage ids to human-readable names which describe them as a state
+ * @var array
+ */
+ public static $stageNames = [
+ self::STAGE_PROPOSED => 'proposed',
+ self::STAGE_REJECTED => 'rejected',
+ self::STAGE_EXPIRED => 'expired',
+ self::STAGE_APPROVED => 'approved',
+ self::STAGE_DISABLED => 'disabled',
+ ];
+
+ /**
+ * Maps stage ids to human-readable names which describe them as an action (which would result
+ * in that stage)
+ * @var array
+ */
+ public static $stageActionNames = [
+ self::STAGE_PROPOSED => 'propose',
+ self::STAGE_REJECTED => 'reject',
+ self::STAGE_EXPIRED => 'propose',
+ self::STAGE_APPROVED => 'approve',
+ self::STAGE_DISABLED => 'disable',
+ ];
+
+ /**
+ * Get member => db field mapping
+ * Loads all fields to avoid unnecessary querying
+ *
+ * @return array
+ */
+ protected static function getSchema() {
+ return [
+ 'table' => 'oauth_registered_consumer',
+ 'fieldColumnMap' => [
+ 'id' => 'oarc_id',
+ 'consumerKey' => 'oarc_consumer_key',
+ 'name' => 'oarc_name',
+ 'userId' => 'oarc_user_id',
+ 'version' => 'oarc_version',
+ 'callbackUrl' => 'oarc_callback_url',
+ 'callbackIsPrefix' => 'oarc_callback_is_prefix',
+ 'description' => 'oarc_description',
+ 'email' => 'oarc_email',
+ 'emailAuthenticated' => 'oarc_email_authenticated',
+ 'oauthVersion' => 'oarc_oauth_version',
+ 'developerAgreement' => 'oarc_developer_agreement',
+ 'ownerOnly' => 'oarc_owner_only',
+ 'wiki' => 'oarc_wiki',
+ 'grants' => 'oarc_grants',
+ 'registration' => 'oarc_registration',
+ 'secretKey' => 'oarc_secret_key',
+ 'rsaKey' => 'oarc_rsa_key',
+ 'restrictions' => 'oarc_restrictions',
+ 'stage' => 'oarc_stage',
+ 'stageTimestamp' => 'oarc_stage_timestamp',
+ 'deleted' => 'oarc_deleted',
+ 'oauth2IsConfidential' => 'oarc_oauth2_is_confidential',
+ 'oauth2GrantTypes' => 'oarc_oauth2_allowed_grants',
+ ],
+ 'idField' => 'id',
+ 'autoIncrField' => 'id',
+ ];
+ }
+
+ protected static function getFieldPermissionChecks() {
+ return [
+ 'name' => 'userCanSee',
+ 'userId' => 'userCanSee',
+ 'version' => 'userCanSee',
+ 'callbackUrl' => 'userCanSee',
+ 'callbackIsPrefix' => 'userCanSee',
+ 'description' => 'userCanSee',
+ 'rsaKey' => 'userCanSee',
+ 'email' => 'userCanSeeEmail',
+ 'secretKey' => 'userCanSeeSecret',
+ 'restrictions' => 'userCanSeePrivate',
+ ];
+ }
+
+ /**
+ * @param array $data
+ * @return string
+ */
+ protected static function getConsumerClass( array $data ) {
+ return static::isOAuth2( $data ) ?
+ OAuth2Client::class :
+ OAuth1Consumer::class;
+ }
+
+ /**
+ * @param array $data
+ * @return bool
+ */
+ protected static function isOAuth2( array $data = [] ) {
+ $oauthVersion = $data['oarc_oauth_version'] ?? $data['oauthVersion'];
+ return (int)$oauthVersion === self::OAUTH_VERSION_2;
+ }
+
+ /**
+ * @param DBConnRef $db
+ * @param string $key
+ * @param int $flags MWOAuthConsumer::READ_* bitfield
+ * @return Consumer|bool
+ */
+ public static function newFromKey( DBConnRef $db, $key, $flags = 0 ) {
+ $row = $db->selectRow( static::getTable(),
+ array_values( static::getFieldColumnMap() ),
+ [ 'oarc_consumer_key' => (string)$key ],
+ __METHOD__,
+ ( $flags & self::READ_LOCKING ) ? [ 'FOR UPDATE' ] : []
+ );
+
+ if ( $row ) {
+ return static::newFromRow( $db, $row );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param DBConnRef $db
+ * @param string $name
+ * @param string $version
+ * @param int $userId Central user ID
+ * @param int $flags MWOAuthConsumer::READ_* bitfield
+ * @return Consumer|bool
+ */
+ public static function newFromNameVersionUser(
+ DBConnRef $db, $name, $version, $userId, $flags = 0
+ ) {
+ $row = $db->selectRow( static::getTable(),
+ array_values( static::getFieldColumnMap() ),
+ [
+ 'oarc_name' => (string)$name,
+ 'oarc_version' => (string)$version,
+ 'oarc_user_id' => (int)$userId
+ ],
+ __METHOD__,
+ ( $flags & self::READ_LOCKING ) ? [ 'FOR UPDATE' ] : []
+ );
+
+ if ( $row ) {
+ return static::newFromRow( $db, $row );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public static function newGrants() {
+ return [];
+ }
+
+ /**
+ * @return array
+ */
+ public static function getAllStages() {
+ return [
+ self::STAGE_PROPOSED,
+ self::STAGE_REJECTED,
+ self::STAGE_EXPIRED,
+ self::STAGE_APPROVED,
+ self::STAGE_DISABLED,
+ ];
+ }
+
+ /**
+ * Internal ID (DB primary key).
+ * @return int
+ */
+ public function getId() {
+ return $this->get( 'id' );
+ }
+
+ /**
+ * Consumer key (32-character hexadecimal string that's used in the OAuth protocol
+ * and in URLs). This is used as the consumer ID for most external purposes.
+ * @return string
+ */
+ public function getConsumerKey() {
+ return $this->get( 'consumerKey' );
+ }
+
+ /**
+ * Name of the consumer.
+ * @return string
+ */
+ public function getName() {
+ return $this->get( 'name' );
+ }
+
+ /**
+ * Central ID of the owner.
+ * @return int
+ */
+ public function getUserId() {
+ return $this->get( 'userId' );
+ }
+
+ /**
+ * Consumer version. This is mostly meant for humans: different versions of the same
+ * application have different keys and are handled as different consumers internally.
+ * @return string
+ */
+ public function getVersion() {
+ return $this->get( 'version' );
+ }
+
+ /**
+ * Callback URL (or prefix). The browser will be redirected to this URL at the end of
+ * an OAuth handshake. See getCallbackIsPrefix() for the interpretation of this field.
+ * @return string
+ */
+ public function getCallbackUrl() {
+ return $this->get( 'callbackUrl' );
+ }
+
+ /**
+ * When true, getCallbackUrl() returns a prefix; the callback URL can be provided by the caller
+ * as long as the prefix matches. When false, the callback URL will be determined by
+ * getCallbackUrl().
+ * @return bool
+ */
+ public function getCallbackIsPrefix() {
+ return $this->get( 'callbackIsPrefix' );
+ }
+
+ /**
+ * Description of the consumer. Currently interpreted as plain text; might change to wikitext
+ * in the future.
+ * @return string
+ */
+ public function getDescription() {
+ return $this->get( 'description' );
+ }
+
+ /**
+ * Email address of the owner.
+ * @return string
+ */
+ public function getEmail() {
+ return $this->get( 'email' );
+ }
+
+ /**
+ * Date of verifying the email, in TS_MW format. In practice this will be the same as
+ * getRegistration().
+ * @return string
+ */
+ public function getEmailAuthenticated() {
+ return $this->get( 'emailAuthenticated' );
+ }
+
+ /**
+ * Did the user accept the developer agreement (the terms of use checkbox at the bottom of the
+ * registration form)? Except for very old users, always true.
+ * @return bool
+ */
+ public function getDeveloperAgreement() {
+ return $this->get( 'developerAgreement' );
+ }
+
+ /**
+ * Owner-only consumers will use one-legged flow instead of three-legged (see
+ * https://github.com/Mashape/mashape-oauth/blob/master/FLOWS.md#oauth-10a-one-legged ); there
+ * is only one user (who is the same as the owner) and they learn the access token at
+ * consumer registration time.
+ * @return bool
+ */
+ public function getOwnerOnly() {
+ return $this->get( 'ownerOnly' );
+ }
+
+ /**
+ * @return int
+ */
+ abstract public function getOAuthVersion();
+
+ /**
+ * The wiki on which the consumer is allowed to access user accounts. A wiki ID or '*' for all.
+ * @return string
+ */
+ public function getWiki() {
+ return $this->get( 'wiki' );
+ }
+
+ /**
+ * The list of grants required by this application.
+ * @return string[]
+ */
+ public function getGrants() {
+ return $this->get( 'grants' );
+ }
+
+ /**
+ * Consumer registration date in TS_MW format.
+ * @return string
+ */
+ public function getRegistration() {
+ return $this->get( 'registration' );
+ }
+
+ /**
+ * Secret key used to derive the consumer secret for HMAC-SHA1 signed OAuth requests.
+ * The actual consumer secret will be calculated via MWOAuthUtils::hmacDBSecret() to mitigate
+ * DB leaks.
+ * @return string
+ */
+ public function getSecretKey() {
+ return $this->get( 'secretKey' );
+ }
+
+ /**
+ * Public RSA key for RSA-SHA1 signerd OAuth requests.
+ * @return string
+ */
+ public function getRsaKey() {
+ return $this->get( 'rsaKey' );
+ }
+
+ /**
+ * Application restrictions (such as allowed IPs).
+ * @return \MWRestrictions
+ */
+ public function getRestrictions() {
+ return $this->get( 'restrictions' );
+ }
+
+ /**
+ * Stage at which the consumer is in the review workflow (proposed, approved etc).
+ * @return int One of the STAGE_* constants
+ */
+ public function getStage() {
+ return $this->get( 'stage' );
+ }
+
+ /**
+ * Date at which the consumer was moved to the current stage, in TS_MW format.
+ * @return string
+ */
+ public function getStageTimestamp() {
+ return $this->get( 'stageTimestamp' );
+ }
+
+ /**
+ * Is the consumer suppressed? (There is no plain deletion; the closest equivalent is the
+ * rejected/disabled stage.)
+ * @return bool
+ */
+ public function getDeleted() {
+ return $this->get( 'deleted' );
+ }
+
+ /**
+ * @param MWOAuthDataStore $dataStore
+ * @param string $verifyCode verification code
+ * @param string $requestKey original request key from /initiate
+ * @return string the url for redirection
+ */
+ public function generateCallbackUrl( $dataStore, $verifyCode, $requestKey ) {
+ $callback = $dataStore->getCallbackUrl( $this->key, $requestKey );
+
+ if ( $callback === 'oob' ) {
+ $callback = $this->getCallbackUrl();
+ }
+
+ return wfAppendQuery( $callback, [
+ 'oauth_verifier' => $verifyCode,
+ 'oauth_token' => $requestKey
+ ] );
+ }
+
+ /**
+ * Attempts to find an authorization by this user for this consumer. Since a user can
+ * accept a consumer multiple times (once for "*" and once for each specific wiki),
+ * there can several access tokens per-wiki (with varying grants) for a consumer.
+ * This will choose the most wiki-specific access token. The precedence is:
+ * a) The acceptance for wiki X if the consumer is applicable only to wiki X
+ * b) The acceptance for wiki $wikiId (if the consumer is applicable to it)
+ * c) The acceptance for wikis "*" (all wikis)
+ *
+ * Users might want more grants on some wikis than on "*". Note that the reverse would not
+ * make sense, since the consumer could just use the "*" acceptance if it has more grants.
+ *
+ * @param \User $mwUser (local wiki user) User who may or may not have authorizations
+ * @param string $wikiId
+ * @throws MWOAuthException
+ * @return ConsumerAcceptance|bool
+ */
+ public function getCurrentAuthorization( User $mwUser, $wikiId ) {
+ $dbr = Utils::getCentralDB( DB_REPLICA );
+
+ $centralUserId = Utils::getCentralIdFromLocalUser( $mwUser );
+ if ( !$centralUserId ) {
+ throw new MWOAuthException(
+ 'mwoauthserver-invalid-user',
+ [
+ $this->getName(),
+ \Message::rawParam(
+ \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E008',
+ 'E008',
+ true
+ )
+ )
+ ]
+ );
+ }
+
+ $checkWiki = $this->getWiki() !== '*' ? $this->getWiki() : $wikiId;
+
+ $cmra = ConsumerAcceptance::newFromUserConsumerWiki(
+ $dbr,
+ $centralUserId,
+ $this,
+ $checkWiki,
+ 0,
+ $this->getOAuthVersion()
+ );
+ if ( !$cmra ) {
+ $cmra = ConsumerAcceptance::newFromUserConsumerWiki(
+ $dbr,
+ $centralUserId,
+ $this,
+ '*',
+ 0,
+ $this->getOAuthVersion()
+ );
+ }
+ return $cmra;
+ }
+
+ /**
+ * @param User $mwUser
+ * @param bool $update
+ * @param array $grants
+ * @param string|null $requestTokenKey
+ * @return mixed
+ */
+ abstract public function authorize( User $mwUser, $update, $grants, $requestTokenKey = null );
+
+ /**
+ * Verify that this user can authorize this consumer
+ *
+ * @param User $mwUser
+ * @throws MWOAuthException
+ * @throws MWException
+ */
+ protected function conductAuthorizationChecks( User $mwUser ) {
+ global $wgBlockDisablesLogin;
+
+ // Check that user and consumer are in good standing
+ if ( $mwUser->isLocked() || $wgBlockDisablesLogin && $mwUser->isBlocked() ) {
+ throw new MWOAuthException( 'mwoauthserver-insufficient-rights', [
+ \Message::rawParam( \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E007',
+ 'E007',
+ true
+ ) )
+ ] );
+ }
+
+ if ( $this->getDeleted() ) {
+ throw new MWOAuthException( 'mwoauthserver-bad-consumer-key', [
+ \Message::rawParam( \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E006',
+ 'E006',
+ true
+ ) )
+ ] );
+ } elseif ( !$this->isUsableBy( $mwUser ) ) {
+ $owner = Utils::getCentralUserNameFromId(
+ $this->getUserId(),
+ $mwUser
+ );
+ throw new MWOAuthException(
+ 'mwoauthserver-bad-consumer',
+ [ $this->getName(), Utils::getCentralUserTalk( $owner ), \Message::rawParam(
+ \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E005',
+ 'E005',
+ true
+ )
+ ) ]
+ );
+ } elseif ( $this->getOwnerOnly() ) {
+ throw new MWOAuthException( 'mwoauthserver-consumer-owner-only', [
+ $this->getName(),
+ \SpecialPage::getTitleFor(
+ 'OAuthConsumerRegistration', 'update/' . $this->getConsumerKey()
+ ),
+ \Message::rawParam( \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E010',
+ 'E010',
+ true
+ ) )
+ ] );
+ }
+ }
+
+ /**
+ * @param User $mwUser
+ * @param bool $update
+ * @param array $grants
+ * @return ConsumerAcceptance
+ * @throws MWOAuthException
+ * @throws MWException
+ */
+ protected function saveAuthorization( User $mwUser, $update, $grants ) {
+ // CentralAuth may abort here if there is no global account for this user
+ $centralUserId = Utils::getCentralIdFromLocalUser( $mwUser );
+ if ( !$centralUserId ) {
+ throw new MWOAuthException(
+ 'mwoauthserver-invalid-user',
+ [
+ $this->getName(),
+ \Message::rawParam(
+ \Linker::makeExternalLink(
+ 'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E008',
+ 'E008',
+ true
+ )
+ )
+ ]
+ );
+ }
+
+ $dbw = Utils::getCentralDB( DB_MASTER );
+ // Check if this authorization exists
+ $cmra = $this->getCurrentAuthorization( $mwUser, wfWikiID() );
+
+ if ( $update ) {
+ // This should be an update to an existing authorization
+ if ( !$cmra ) {
+ // update requested, but no existing key
+ throw new MWOAuthException( 'mwoauthserver-invalid-request' );
+ }
+ $cmra->setFields( [
+ 'wiki' => $this->getWiki(),
+ 'grants' => $grants
+ ] );
+ $cmra->save( $dbw );
+ } elseif ( !$cmra ) {
+ // Add the Authorization to the database
+ $accessToken = MWOAuthDataStore::newToken();
+ $cmra = ConsumerAcceptance::newFromArray( [
+ 'id' => null,
+ 'wiki' => $this->getWiki(),
+ 'userId' => $centralUserId,
+ 'consumerId' => $this->getId(),
+ 'accessToken' => $accessToken->key,
+ 'accessSecret' => $accessToken->secret,
+ 'grants' => $grants,
+ 'accepted' => wfTimestampNow(),
+ 'oauth_version' => $this->getOAuthVersion()
+ ] );
+ $cmra->save( $dbw );
+ }
+
+ return $cmra;
+ }
+
+ /**
+ * Check if the consumer is usable by $user
+ *
+ * "Usable by $user" includes:
+ * - Approved for multi-user use
+ * - Approved for owner-only use and is owned by $user
+ * - Still pending approval and is owned by $user
+ *
+ * @param \User $user
+ * @return bool
+ */
+ public function isUsableBy( \User $user ) {
+ if ( $this->stage === self::STAGE_APPROVED && !$this->getOwnerOnly() ) {
+ return true;
+ } elseif ( $this->stage === self::STAGE_PROPOSED || $this->stage === self::STAGE_APPROVED ) {
+ $centralId = Utils::getCentralIdFromLocalUser( $user );
+ return ( $centralId && $this->userId === $centralId );
+ }
+
+ return false;
+ }
+
+ protected function normalizeValues() {
+ // Keep null values since we're constructing w/ them to auto-increment
+ $this->id = $this->id === null ? null : (int)$this->id;
+ $this->userId = (int)$this->userId;
+ $this->registration = wfTimestamp( TS_MW, $this->registration );
+ $this->stage = (int)$this->stage;
+ $this->stageTimestamp = wfTimestamp( TS_MW, $this->stageTimestamp );
+ $this->emailAuthenticated = wfTimestamp( TS_MW, $this->emailAuthenticated );
+ $this->grants = (array)$this->grants; // sanity
+ $this->callbackIsPrefix = (bool)$this->callbackIsPrefix;
+ $this->ownerOnly = (bool)$this->ownerOnly;
+ $this->oauthVersion = (int)$this->oauthVersion;
+ $this->developerAgreement = (bool)$this->developerAgreement;
+ $this->deleted = (bool)$this->deleted;
+ $this->oauth2IsConfidential = (bool)$this->oauth2IsConfidential;
+ }
+
+ protected function encodeRow( DBConnRef $db, $row ) {
+ // For compatibility with other wikis in the farm, un-remap some grants
+ foreach ( self::$mapBackCompatGrants as $old => $new ) {
+ while ( ( $i = array_search( $new, $row['oarc_grants'], true ) ) !== false ) {
+ $row['oarc_grants'][$i] = $old;
+ }
+ }
+
+ $row['oarc_registration'] = $db->timestamp( $row['oarc_registration'] );
+ $row['oarc_stage_timestamp'] = $db->timestamp( $row['oarc_stage_timestamp'] );
+ $row['oarc_restrictions'] = $row['oarc_restrictions']->toJson();
+ $row['oarc_grants'] = \FormatJson::encode( $row['oarc_grants'] );
+ $row['oarc_email_authenticated'] =
+ $db->timestampOrNull( $row['oarc_email_authenticated'] );
+ $row['oarc_oauth2_allowed_grants'] = FormatJson::encode(
+ $row['oarc_oauth2_allowed_grants']
+ );
+ return $row;
+ }
+
+ protected function decodeRow( DBConnRef $db, $row ) {
+ $row['oarc_registration'] = wfTimestamp( TS_MW, $row['oarc_registration'] );
+ $row['oarc_stage'] = (int)$row['oarc_stage'];
+ $row['oarc_stage_timestamp'] = wfTimestamp( TS_MW, $row['oarc_stage_timestamp'] );
+ $row['oarc_restrictions'] = \MWRestrictions::newFromJson( $row['oarc_restrictions'] );
+ $row['oarc_grants'] = \FormatJson::decode( $row['oarc_grants'], true );
+ $row['oarc_user_id'] = (int)$row['oarc_user_id'];
+ $row['oarc_email_authenticated'] =
+ wfTimestampOrNull( TS_MW, $row['oarc_email_authenticated'] );
+ $row['oarc_oauth2_allowed_grants'] = FormatJson::decode(
+ $row['oarc_oauth2_allowed_grants'], true
+ );
+
+ // For backwards compatibility, remap some grants
+ foreach ( self::$mapBackCompatGrants as $old => $new ) {
+ while ( ( $i = array_search( $old, $row['oarc_grants'], true ) ) !== false ) {
+ $row['oarc_grants'][$i] = $new;
+ }
+ }
+
+ return $row;
+ }
+
+ /**
+ * Magic method so that fields like $consumer->secret and $consumer->key work.
+ * This allows MWOAuthConsumer to be a replacement for OAuthConsumer
+ * in lib/OAuth.php without inheriting.
+ * @param mixed $prop
+ * @return mixed
+ */
+ public function __get( $prop ) {
+ if ( $prop === 'key' ) {
+ return $this->consumerKey;
+ } elseif ( $prop === 'secret' ) {
+ return Utils::hmacDBSecret( $this->secretKey );
+ } elseif ( $prop === 'callback_url' ) {
+ return $this->callbackUrl;
+ } else {
+ throw new \LogicException( 'Direct property access attempt: ' . $prop );
+ }
+ }
+
+ protected function userCanSee( $name, \IContextSource $context ) {
+ if ( $this->getDeleted()
+ && !$context->getUser()->isAllowed( 'mwoauthviewsuppressed' )
+ ) {
+ return $context->msg( 'mwoauth-field-hidden' );
+ } else {
+ return true;
+ }
+ }
+
+ protected function userCanSeePrivate( $name, \IContextSource $context ) {
+ if ( !$context->getUser()->isAllowed( 'mwoauthviewprivate' ) ) {
+ return $context->msg( 'mwoauth-field-private' );
+ } else {
+ return $this->userCanSee( $name, $context );
+ }
+ }
+
+ protected function userCanSeeEmail( $name, \IContextSource $context ) {
+ if ( !$context->getUser()->isAllowed( 'mwoauthmanageconsumer' ) ) {
+ return $context->msg( 'mwoauth-field-private' );
+ } else {
+ return $this->userCanSee( $name, $context );
+ }
+ }
+
+ protected function userCanSeeSecret( $name, \IContextSource $context ) {
+ return $context->msg( 'mwoauth-field-private' );
+ }
+}