diff options
Diffstat (limited to 'OAuth/src/Backend/Utils.php')
-rw-r--r-- | OAuth/src/Backend/Utils.php | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/OAuth/src/Backend/Utils.php b/OAuth/src/Backend/Utils.php new file mode 100644 index 00000000..7c6bafd7 --- /dev/null +++ b/OAuth/src/Backend/Utils.php @@ -0,0 +1,471 @@ +<?php + +namespace MediaWiki\Extensions\OAuth\Backend; + +use EchoEvent; +use MediaWiki\Extensions\OAuth\Lib\OAuthSignatureMethod_HMAC_SHA1; +use MediaWiki\MediaWikiServices; +use User; +use Wikimedia\Rdbms\DBConnRef; +use Wikimedia\Rdbms\IDatabase; + +/** + * Static utility functions for OAuth + * + * @file + * @ingroup OAuth + */ +class Utils { + /** + * @return bool + */ + public static function isCentralWiki() { + global $wgMWOAuthCentralWiki; + + return ( wfWikiId() === $wgMWOAuthCentralWiki ); + } + + /** + * @param int $index DB_MASTER/DB_REPLICA + * @return DBConnRef + */ + public static function getCentralDB( $index ) { + global $wgMWOAuthCentralWiki, $wgMWOAuthReadOnly; + + $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + + // T244415: Use the master if there were changes + if ( $index === DB_REPLICA && $lbFactory->hasOrMadeRecentMasterChanges() ) { + $index = DB_MASTER; + } + + $db = $lbFactory->getMainLB( $wgMWOAuthCentralWiki )->getLazyConnectionRef( + $index, [], $wgMWOAuthCentralWiki ); + $db->daoReadOnly = $wgMWOAuthReadOnly; + return $db; + } + + /** + * @return \BagOStuff + */ + public static function getSessionCache() { + global $wgMWOAuthSessionCacheType; + global $wgSessionCacheType; + + $sessionCacheType = $wgMWOAuthSessionCacheType ?? $wgSessionCacheType; + return \ObjectCache::getInstance( $sessionCacheType ); + } + + /** + * @param DBConnRef $db + * @return array + */ + public static function getConsumerStateCounts( DBConnRef $db ) { + $res = $db->select( 'oauth_registered_consumer', + [ 'oarc_stage', 'count' => 'COUNT(*)' ], + [], + __METHOD__, + [ 'GROUP BY' => 'oarc_stage' ] + ); + $table = [ + Consumer::STAGE_APPROVED => 0, + Consumer::STAGE_DISABLED => 0, + Consumer::STAGE_EXPIRED => 0, + Consumer::STAGE_PROPOSED => 0, + Consumer::STAGE_REJECTED => 0, + ]; + foreach ( $res as $row ) { + $table[(int)$row->oarc_stage] = (int)$row->count; + } + return $table; + } + + /** + * Get request headers. + * Sanitizes the output of apache_request_headers because + * we always want the keys to be Cased-Like-This and arh() + * returns the headers in the same case as they are in the + * request + * @return array Header name => value + */ + public static function getHeaders() { + $request = \RequestContext::getMain()->getRequest(); + $headers = $request->getAllHeaders(); + + $out = []; + foreach ( $headers as $key => $value ) { + $key = str_replace( + " ", + "-", + ucwords( strtolower( str_replace( "-", " ", $key ) ) ) + ); + $out[$key] = $value; + } + return $out; + } + + /** + * Test this request for an OAuth Authorization header + * @param \WebRequest $request the MediaWiki request + * @return bool true if a header was found + */ + public static function hasOAuthHeaders( \WebRequest $request ) { + $header = $request->getHeader( 'Authorization' ); + if ( $header !== false && substr( $header, 0, 6 ) == 'OAuth ' ) { + return true; + } + return false; + } + + /** + * Make a cache key for the given arguments, that (hopefully) won't clash with + * anything else in your cache + * @param string ...$args + * @return string + */ + public static function getCacheKey( ...$args ) { + global $wgMWOAuthCentralWiki; + + return "OAUTH:$wgMWOAuthCentralWiki:" . implode( ':', $args ); + } + + /** + * @param DBConnRef $dbw + * @return void + */ + public static function runAutoMaintenance( DBConnRef $dbw ) { + global $wgMWOAuthRequestExpirationAge; + + if ( $wgMWOAuthRequestExpirationAge <= 0 ) { + return; + } + + $cutoff = time() - $wgMWOAuthRequestExpirationAge; + $fname = __METHOD__; + \DeferredUpdates::addUpdate( + new \AutoCommitUpdate( + $dbw, + __METHOD__, + function ( IDatabase $dbw ) use ( $cutoff, $fname ) { + $dbw->update( + 'oauth_registered_consumer', + [ + 'oarc_stage' => Consumer::STAGE_EXPIRED, + 'oarc_stage_timestamp' => $dbw->timestamp() + ], + [ + 'oarc_stage' => Consumer::STAGE_PROPOSED, + 'oarc_stage_timestamp < ' . + $dbw->addQuotes( $dbw->timestamp( $cutoff ) ) + ], + $fname + ); + } + ) + ); + } + + /** + * Get the pretty name of an OAuth wiki ID restriction value + * + * @param string $wikiId A wiki ID or '*' + * @return string + */ + public static function getWikiIdName( $wikiId ) { + if ( $wikiId === '*' ) { + return wfMessage( 'mwoauth-consumer-allwikis' )->text(); + } else { + $host = \WikiMap::getWikiName( $wikiId ); + if ( strpos( $host, '.' ) ) { + return $host; // e.g. "en.wikipedia.org" + } else { + return $wikiId; + } + } + } + + /** + * Get the pretty names of all local wikis + * + * @return array associative array of local wiki names indexed by wiki ID + */ + public static function getAllWikiNames() { + global $wgConf; + $wikiNames = []; + foreach ( $wgConf->getLocalDatabases() as $dbname ) { + $name = self::getWikiIdName( $dbname ); + if ( $name != $dbname ) { + $wikiNames[$dbname] = $name; + } + } + return $wikiNames; + } + + /** + * Quickly get a new server with all the default configurations + * + * @return MWOAuthServer with default configurations + */ + public static function newMWOAuthServer() { + $store = static::newMWOAuthDataStore(); + $server = new MWOAuthServer( $store ); + $server->add_signature_method( new OAuthSignatureMethod_HMAC_SHA1() ); + $server->add_signature_method( new MWOAuthSignatureMethod_RSA_SHA1( $store ) ); + + return $server; + } + + public static function newMWOAuthDataStore() { + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $dbr = self::getCentralDB( DB_REPLICA ); + $dbw = $lb->getServerCount() > 1 ? self::getCentralDB( DB_MASTER ) : null; + return new MWOAuthDataStore( $dbr, $dbw, self::getSessionCache() ); + } + + /** + * Given a central wiki user ID, get a central user name + * + * @param int $userId + * @param bool|\User|string $audience show hidden names based on this user, or false for public + * @throws \MWException + * @return string|bool User name, false if not found, empty string if name is hidden + */ + public static function getCentralUserNameFromId( $userId, $audience = false ) { + global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; + + if ( $wgMWOAuthSharedUserIDs ) { // global ID required via hook + $lookup = \CentralIdLookup::factory( $wgMWOAuthSharedUserSource ); + $name = $lookup->nameFromCentralId( + $userId, + $audience === 'raw' + ? \CentralIdLookup::AUDIENCE_RAW + : ( $audience ?: \CentralIdLookup::AUDIENCE_PUBLIC ) + ); + if ( $name === null ) { + $name = false; + } + } else { + $name = ''; + $user = \User::newFromId( $userId ); + if ( $audience === 'raw' + || !$user->isHidden() + || ( $audience instanceof \User && $audience->isAllowed( 'hideuser' ) ) + ) { + $name = $user->getName(); + } + } + + return $name; + } + + /** + * Given a central wiki user ID, get a local User object + * + * @param int $userId + * @throws \MWException + * @return \User|bool User or false if not found + */ + public static function getLocalUserFromCentralId( $userId ) { + global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; + + if ( $wgMWOAuthSharedUserIDs ) { // global ID required via hook + $lookup = \CentralIdLookup::factory( $wgMWOAuthSharedUserSource ); + $user = $lookup->localUserFromCentralId( $userId ); + if ( $user === null || !$lookup->isAttached( $user ) ) { + $user = false; + } + } else { + $user = \User::newFromId( $userId ); + } + + return $user; + } + + /** + * Given a local User object, get the user ID for that user on the central wiki + * + * @param \User $user + * @throws \MWException + * @return int|bool ID or false if not found + */ + public static function getCentralIdFromLocalUser( \User $user ) { + global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; + + if ( $wgMWOAuthSharedUserIDs ) { // global ID required via hook + // T227688 do not rely on array autocreation for non-stdClass + if ( !isset( $user->oAuthUserData ) ) { + $user->oAuthUserData = []; + } + + if ( isset( $user->oAuthUserData['centralId'] ) ) { + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable + $id = $user->oAuthUserData['centralId']; + } else { + $lookup = \CentralIdLookup::factory( $wgMWOAuthSharedUserSource ); + if ( !$lookup->isAttached( $user ) ) { + $id = false; + } else { + $id = $lookup->centralIdFromLocalUser( $user ); + if ( $id === 0 ) { + $id = false; + } + } + // Process cache the result to avoid queries + $user->oAuthUserData['centralId'] = $id; + } + } else { + $id = $user->getId(); + } + + return $id; + } + + /** + * Given a username, get the user ID for that user on the central wiki. + * @param string $username + * @throws \MWException + * @return int|bool ID or false if not found + */ + public static function getCentralIdFromUserName( $username ) { + global $wgMWOAuthSharedUserIDs, $wgMWOAuthSharedUserSource; + + if ( $wgMWOAuthSharedUserIDs ) { // global ID required via hook + $lookup = \CentralIdLookup::factory( $wgMWOAuthSharedUserSource ); + $id = $lookup->centralIdFromName( $username ); + if ( $id === 0 ) { + $id = false; + } + } else { + $id = false; + $user = \User::newFromName( $username ); + if ( $user instanceof \User && $user->getId() > 0 ) { + $id = $user->getId(); + } + } + + return $id; + } + + /** + * Get the effective secret key/token to use for OAuth purposes. + * + * For example, the "secret key" and "access secret" values that are + * used for authenticating request should be the result of applying this + * function to the respective values stored in the DB. This means that + * a leak of DB values is not enough to impersonate consumers. + * + * @param string $secret + * @return string + */ + public static function hmacDBSecret( $secret ) { + global $wgOAuthSecretKey, $wgSecretKey; + + if ( empty( $wgOAuthSecretKey ) ) { + $secretKey = $wgSecretKey; + } else { + $secretKey = $wgOAuthSecretKey; + } + + return $secretKey ? hash_hmac( 'sha1', $secret, $secretKey ) : $secret; + } + + /** + * Get a link to the central wiki's user talk page of a user. + * + * @param string $username the username of the User Talk link + * @return string the (proto-relative, urlencoded) url of the central wiki's user talk page + */ + public static function getCentralUserTalk( $username ) { + global $wgMWOAuthCentralWiki, $wgMWOAuthSharedUserIDs; + + if ( $wgMWOAuthSharedUserIDs ) { + $url = \WikiMap::getForeignURL( + $wgMWOAuthCentralWiki, + "User_talk:$username" + ); + } else { + $url = \Title::makeTitleSafe( NS_USER_TALK, $username )->getFullURL(); + } + return $url; + } + + /** + * @param array $grants + * @return bool + */ + public static function grantsAreValid( array $grants ) { + // Remove our special grants before calling the core method + $grants = array_diff( $grants, [ 'mwoauth-authonly', 'mwoauth-authonlyprivate' ] ); + return \MWGrants::grantsAreValid( $grants ); + } + + /** + * Given an OAuth consumer stage change event, find out who needs to be notified. + * Will be used as an EchoAttributeManager::ATTR_LOCATORS callback. + * @param EchoEvent $event + * @return User[] + */ + public static function locateUsersToNotify( EchoEvent $event ) { + $agent = $event->getAgent(); + $owner = self::getLocalUserFromCentralId( $event->getExtraParam( 'owner-id' ) ); + + $users = []; + switch ( $event->getType() ) { + case 'oauth-app-propose': + // notify OAuth admins about new proposed apps + $oauthAdmins = self::getOAuthAdmins(); + foreach ( $oauthAdmins as $admin ) { + if ( $admin->equals( $owner ) ) { + continue; + } + $users[$admin->getId()] = $admin; + } + break; + case 'oauth-app-update': + case 'oauth-app-approve': + case 'oauth-app-reject': + case 'oauth-app-disable': + case 'oauth-app-reenable': + // notify owner if someone else changed the status of the app + if ( !$owner->equals( $agent ) ) { + $users[$owner->getId()] = $owner; + } + break; + } + return $users; + } + + /** + * Get the change tag name for a given consumer. + * @param int $consumerId + * @return string + */ + public static function getTagName( $consumerId ) { + return 'OAuth CID: ' . (int)$consumerId; + } + + /** + * Check if a given change tag name should be reserved for this extension. + * @param string $tagName + * @return bool + */ + public static function isReservedTagName( $tagName ) { + return strpos( strtolower( $tagName ), 'oauth cid:' ) === 0; + } + + /** + * Return a list of all OAuth admins (or the first 5000 in the unlikely case that there is more + * than that). + * Should be called on the central OAuth wiki. + * @return User[] + */ + protected static function getOAuthAdmins() { + global $wgOAuthGroupsToNotify; + + if ( !$wgOAuthGroupsToNotify ) { + return []; + } + + return iterator_to_array( User::findUsersByGroup( $wgOAuthGroupsToNotify ) ); + } +} |