Dpatrick has submitted this change and it was merged. Change subject: Send notifications about app management events ......................................................................
Send notifications about app management events Send an Echo notification to all OAuth admins when a new app is proposed or an existing one updated, and a notification to the app owner when an app is approved, rejected, disabled or reenabled. Bug: T62528 Bug: T61772 Change-Id: Iee6e0074d648236b01ae40da704a27cfb94d4748 Depends-On: Idbaf2102410f82343ad38ff51ccb7ab70b15e654 --- M OAuth.config.php M OAuth.setup.php M backend/MWOAuthConsumer.php M backend/MWOAuthUtils.php M backend/schema/mysql/OAuth.sql M control/MWOAuthConsumerSubmitControl.php A frontend/EchoOAuthStageChangePresentationModel.php M frontend/MWOAuthUI.hooks.php M frontend/MWOAuthUI.setup.php A frontend/assets/echo-icon.png M i18n/en.json M i18n/qqq.json 12 files changed, 336 insertions(+), 70 deletions(-) Approvals: Dpatrick: Verified; Looks good to me, approved diff --git a/OAuth.config.php b/OAuth.config.php index 3ff16fc..7347ece 100644 --- a/OAuth.config.php +++ b/OAuth.config.php @@ -56,6 +56,11 @@ $wgGroupPermissions['user']['mwoauthmanagemygrants'] = true; +$wgDefaultUserOptions['echo-subscriptions-web-oauth-owner'] = true; +$wgDefaultUserOptions['echo-subscriptions-email-oauth-owner'] = true; +$wgDefaultUserOptions['echo-subscriptions-web-oauth-admin'] = true; +$wgDefaultUserOptions['echo-subscriptions-email-oauth-admin'] = true; + /** @var bool Require HTTPs for user transactions that might send out secret tokens */ $wgMWOAuthSecureTokenTransfer = true; // RfC compliance @@ -80,5 +85,10 @@ */ $wgOAuthSecretKey = $wgSecretKey; +/** + * @var string[] User groups to notify about new consumers that need to be reviewed. + */ +$wgOAuthGroupsToNotify = []; + # End of configuration variables. # ######## diff --git a/OAuth.setup.php b/OAuth.setup.php index 2443c32..b718b21 100644 --- a/OAuth.setup.php +++ b/OAuth.setup.php @@ -77,6 +77,10 @@ $classes['MediaWiki\Extensions\OAuth\MWOAuthServer'] = "$backendDir/MWOAuthServer.php"; // "MWOAuth1Protocol"? $classes['MediaWiki\Extensions\OAuth\MWOAuthSignatureMethod_RSA_SHA1'] = "$backendDir/MWOAuthSignatureMethod.php"; + # Echo + $classes['MediaWiki\Extensions\OAuth\EchoOAuthStageChangePresentationModel'] = + "$frontendDir/EchoOAuthStageChangePresentationModel.php"; + # Library $classes['MediaWiki\Extensions\OAuth\OAuthException'] = "$libDir/OAuth.php"; $classes['MediaWiki\Extensions\OAuth\OAuthConsumer'] = "$libDir/OAuth.php"; diff --git a/backend/MWOAuthConsumer.php b/backend/MWOAuthConsumer.php index 9bbd21d..2289888 100644 --- a/backend/MWOAuthConsumer.php +++ b/backend/MWOAuthConsumer.php @@ -38,7 +38,8 @@ protected $consumerKey; /** @var string Name of connected application */ protected $name; - /** @var integer Publisher user ID (on central wiki) */ + /** @var integer 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; diff --git a/backend/MWOAuthUtils.php b/backend/MWOAuthUtils.php index 7df04fb..23a54a5 100644 --- a/backend/MWOAuthUtils.php +++ b/backend/MWOAuthUtils.php @@ -2,8 +2,9 @@ namespace MediaWiki\Extensions\OAuth; +use EchoEvent; use Hooks; -use MWException; +use User; /** * Static utility functions for OAuth @@ -458,4 +459,55 @@ 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 = MWOAuthUtils::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; + } + + /** + * 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 ) ); + } } diff --git a/backend/schema/mysql/OAuth.sql b/backend/schema/mysql/OAuth.sql index 4edb72b..b0c99d0 100644 --- a/backend/schema/mysql/OAuth.sql +++ b/backend/schema/mysql/OAuth.sql @@ -13,7 +13,7 @@ oarc_consumer_key varbinary(32) NOT NULL, -- Name of the application oarc_name varchar(128) binary NOT NULL, - -- Key to the user who proposed the application + -- (Central) user id of the user who proposed the application oarc_user_id integer unsigned NOT NULL, -- Version of the application oarc_version varbinary(32) NOT NULL, diff --git a/control/MWOAuthConsumerSubmitControl.php b/control/MWOAuthConsumerSubmitControl.php index 7b49fc4..957fed2 100644 --- a/control/MWOAuthConsumerSubmitControl.php +++ b/control/MWOAuthConsumerSubmitControl.php @@ -30,6 +30,13 @@ * @TODO: improve error messages */ class MWOAuthConsumerSubmitControl extends MWOAuthSubmitControl { + /** + * Names of the actions that can be performed on a consumer. These are the same as the + * options in getRequiredFields(). + * @var array + */ + public static $actions = array( 'propose', 'update', 'approve', 'reject', 'disable', 'reenable' ); + /** @var \DBConnRef */ protected $dbw; @@ -241,18 +248,12 @@ ); $cmr->save( $dbw ); - $logEntry = new \ManualLogEntry( - 'mwoauthconsumer', - $cmr->get( 'ownerOnly' ) ? 'create-owner-only' : 'propose' - ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $this->getLogTitle( $dbw, $cmr->get( 'userId' ) ) ); - $logEntry->setComment( $this->vals['description'] ); - $logEntry->setParameters( array( '4:consumer' => $cmr->get( 'consumerKey' ) ) ); - $logEntry->setRelations( array( - 'OAuthConsumer' => array( $cmr->get( 'consumerKey' ) ) - ) ); - $logEntry->insert( $dbw ); + if ( $cmr->get( 'ownerOnly' ) ) { + $this->makeLogEntry( $dbw, $cmr, 'create-owner-only', $user, $this->vals['description'] ); + } else { + $this->makeLogEntry( $dbw, $cmr, $action, $user, $this->vals['description'] ); + $this->notify( $cmr, $user, $action, null ); + } // If it's owner-only, automatically accept it for the user too. $cmra = null; @@ -302,15 +303,8 @@ // Log if something actually changed if ( $cmr->save( $dbw ) ) { - $logEntry = new \ManualLogEntry( 'mwoauthconsumer', 'update' ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $this->getLogTitle( $dbw, $cmr->get( 'userId' ) ) ); - $logEntry->setComment( $this->vals['reason'] ); - $logEntry->setParameters( array( '4:consumer' => $cmr->get( 'consumerKey' ) ) ); - $logEntry->setRelations( array( - 'OAuthConsumer' => array( $cmr->get( 'consumerKey' ) ) - ) ); - $logEntry->insert( $dbw ); + $this->makeLogEntry( $dbw, $cmr, $action, $user, $this->vals['reason'] ); + $this->notify( $cmr, $user, $action, $this->vals['reason'] ); } $cmra = null; @@ -366,17 +360,8 @@ // Log if something actually changed if ( $cmr->save( $dbw ) ) { - $logEntry = new \ManualLogEntry( 'mwoauthconsumer', 'approve' ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $this->getLogTitle( $dbw, $cmr->get( 'userId' ) ) ); - $logEntry->setComment( $this->vals['reason'] ); - $logEntry->setParameters( array( '4:consumer' => $cmr->get( 'consumerKey' ) ) ); - $logEntry->setRelations( array( - 'OAuthConsumer' => array( $cmr->get( 'consumerKey' ) ) - ) ); - $logEntry->insert( $dbw ); - - // @TODO: email/notifications? + $this->makeLogEntry( $dbw, $cmr, $action, $user, $this->vals['reason'] ); + $this->notify( $cmr, $user, $action, $this->vals['reason'] ); } return $this->success( $cmr ); @@ -405,17 +390,8 @@ // Log if something actually changed if ( $cmr->save( $dbw ) ) { - $logEntry = new \ManualLogEntry( 'mwoauthconsumer', 'reject' ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $this->getLogTitle( $dbw, $cmr->get( 'userId' ) ) ); - $logEntry->setComment( $this->vals['reason'] ); - $logEntry->setParameters( array( '4:consumer' => $cmr->get( 'consumerKey' ) ) ); - $logEntry->setRelations( array( - 'OAuthConsumer' => array( $cmr->get( 'consumerKey' ) ) - ) ); - $logEntry->insert( $dbw ); - - // @TODO: email/notifications? + $this->makeLogEntry( $dbw, $cmr, $action, $user, $this->vals['reason'] ); + $this->notify( $cmr, $user, $action, $this->vals['reason'] ); } return $this->success( $cmr ); @@ -446,17 +422,8 @@ // Log if something actually changed if ( $cmr->save( $dbw ) ) { - $logEntry = new \ManualLogEntry( 'mwoauthconsumer', 'disable' ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $this->getLogTitle( $dbw, $cmr->get( 'userId' ) ) ); - $logEntry->setComment( $this->vals['reason'] ); - $logEntry->setParameters( array( '4:consumer' => $cmr->get( 'consumerKey' ) ) ); - $logEntry->setRelations( array( - 'OAuthConsumer' => array( $cmr->get( 'consumerKey' ) ) - ) ); - $logEntry->insert( $dbw ); - - // @TODO: email/notifications? + $this->makeLogEntry( $dbw, $cmr, $action, $user, $this->vals['reason'] ); + $this->notify( $cmr, $user, $action, $this->vals['reason'] ); } return $this->success( $cmr ); @@ -483,17 +450,8 @@ // Log if something actually changed if ( $cmr->save( $dbw ) ) { - $logEntry = new \ManualLogEntry( 'mwoauthconsumer', 'reenable' ); - $logEntry->setPerformer( $user ); - $logEntry->setTarget( $this->getLogTitle( $dbw, $cmr->get( 'userId' ) ) ); - $logEntry->setComment( $this->vals['reason'] ); - $logEntry->setParameters( array( '4:consumer' => $cmr->get( 'consumerKey' ) ) ); - $logEntry->setRelations( array( - 'OAuthConsumer' => array( $cmr->get( 'consumerKey' ) ) - ) ); - $logEntry->insert( $dbw ); - - // @TODO: email/notifications? + $this->makeLogEntry( $dbw, $cmr, $action, $user, $this->vals['reason'] ); + $this->notify( $cmr, $user, $action, $this->vals['reason'] ); } return $this->success( $cmr ); @@ -509,4 +467,50 @@ $name = MWOAuthUtils::getCentralUserNameFromId( $userId ); return \Title::makeTitleSafe( NS_USER, $name ); } + + /** + * @param \DBConnRef $dbw + * @param MWOAuthConsumer $cmr + * @param string $action + * @param \User $performer + * @param string $comment + */ + protected function makeLogEntry( $dbw, MWOAuthConsumer $cmr, $action, \User $performer, $comment ) { + $logEntry = new \ManualLogEntry( 'mwoauthconsumer', $action ); + $logEntry->setPerformer( $performer ); + $logEntry->setTarget( $this->getLogTitle( $dbw, $cmr->get( 'userId' ) ) ); + $logEntry->setComment( $comment ); + $logEntry->setParameters( array( '4:consumer' => $cmr->get( 'consumerKey' ) ) ); + $logEntry->setRelations( array( + 'OAuthConsumer' => array( $cmr->get( 'consumerKey' ) ) + ) ); + $logEntry->insert( $dbw ); + } + + /** + * @param MWOAuthConsumer $cmr Consumer which was the subject of the action + * @param \User $user User who performed the action + * @param string $actionType Action type + * @param string $comment + * @throws \MWException + */ + protected function notify( $cmr, $user, $actionType, $comment ) { + if ( !in_array( $actionType, self::$actions, true ) ) { + throw new \MWException( "Invalid action type: $actionType" ); + } + if ( !class_exists( '\EchoEvent' ) ) { + return; + } + + \EchoEvent::create( [ + 'type' => 'oauth-app-' . $actionType, + 'agent' => $user, + 'extra' => [ + 'action' => $actionType, + 'app-key' => $cmr->get( 'consumerKey' ), + 'owner-id' => $cmr->get( 'userId' ), + 'comment' => $comment, + ], + ] ); + } } diff --git a/frontend/EchoOAuthStageChangePresentationModel.php b/frontend/EchoOAuthStageChangePresentationModel.php new file mode 100644 index 0000000..3137202 --- /dev/null +++ b/frontend/EchoOAuthStageChangePresentationModel.php @@ -0,0 +1,120 @@ +<?php + +namespace MediaWiki\Extensions\OAuth; + +use EchoAttributeManager; +use EchoEventPresentationModel; +use MediaWiki\Logger\LoggerFactory; +use MWException; +use User; +use SpecialPage; + +class EchoOAuthStageChangePresentationModel extends EchoEventPresentationModel { + /** @var User[] OAuth admins who should be notified about additiions to the review queue */ + protected static $oauthAdmins; + + /** @var MWOAuthConsumer|false */ + protected $consumer; + + /** @var User|false The owner of the OAuth consumer */ + protected $owner; + + /** + * Helper function for $wgEchoNotifications + * @param string $action One of the actions from MWOAuthConsumerSubmitControl::$actions + * @return array + */ + public static function getDefinition( $action ) { + if ( $action === 'propose' ) { + // notify admins + $category = 'oauth-admin'; + } else { + // notify owner + $category = 'oauth-owner'; + } + + return [ + EchoAttributeManager::ATTR_LOCATORS => [ MWOAuthUtils::class . '::locateUsersToNotify' ], + 'category' => $category, + 'presentation-model' => self::class, + 'icon' => 'oauth', + ]; + } + + public function getHeaderMessage() { + $action = $this->event->getExtraParam( 'action' ); + return $this->msg( "notification-oauth-app-$action-title", + $this->event->getAgent(), $this->getConsumerName(), $this->getOwner() ); + } + + public function getSubjectMessage() { + $action = $this->event->getExtraParam( 'action' ); + return $this->msg( "notification-oauth-app-$action-subject", + $this->event->getAgent(), $this->getConsumerName(), $this->getOwner() ); + } + + public function getBodyMessage() { + $comment = $this->event->getExtraParam( 'comment' ); + return $comment ? $this->msg( 'notification-oauth-app-body', $comment ) : false; + } + + public function getIconType() { + return 'oauth'; + } + + public function getPrimaryLink() { + $consumerKey = $this->event->getExtraParam( 'app-key' ); + $action = $this->event->getExtraParam( 'action' ); + + if ( $action === 'propose' ) { + // show management interface + $page = SpecialPage::getSafeTitleFor( 'OAuthManageConsumers', $consumerKey ); + } else { + // show public view + $page = SpecialPage::getSafeTitleFor( 'OAuthListConsumers', "view/$consumerKey" ); + } + if ( $page === null ) { + throw new MWException( "Invalid app ID: $consumerKey" ); + } + + return [ + 'url' => $page->getLocalURL(), + 'label' => $this->msg( "notification-oauth-app-$action-primary-link" )->text(), + ]; + } + + public function getSecondaryLinks() { + return [ $this->getAgentLink() ]; + } + + /** + * @return MWOAuthConsumer|false + */ + protected function getConsumer() { + if ( $this->consumer === null ) { + $dbr = MWOAuthUtils::getCentralDB( DB_SLAVE ); + $this->consumer = + MWOAuthConsumer::newFromKey( $dbr, $this->event->getExtraParam( 'app-key' ) ); + } + return $this->consumer; + } + + /** + * @return User|false + */ + protected function getOwner() { + if ( $this->owner === null ) { + $this->owner = MWOAuthUtils::getLocalUserFromCentralId( + $this->event->getExtraParam( 'owner-id' ) ); + } + return $this->owner; + } + + /** + * @return string + */ + protected function getConsumerName() { + $consumer = $this->getConsumer(); + return $consumer ? $consumer->get( 'name' ) : false; + } +} diff --git a/frontend/MWOAuthUI.hooks.php b/frontend/MWOAuthUI.hooks.php index 268ab9b..d71d04d 100644 --- a/frontend/MWOAuthUI.hooks.php +++ b/frontend/MWOAuthUI.hooks.php @@ -173,4 +173,32 @@ } return true; } + + /** + * @param array $notifications + * @param array $notificationCategories + * @param array $icons + */ + public static function onBeforeCreateEchoEvent( &$notifications, &$notificationCategories, &$icons ) { + global $wgOAuthGroupsToNotify; + + $notificationCategories['oauth-owner'] = array( + 'tooltip' => 'echo-pref-tooltip-oauth-owner', + ); + + $notificationCategories['oauth-admin'] = array( + 'tooltip' => 'echo-pref-tooltip-oauth-admin', + 'usergroups' => $wgOAuthGroupsToNotify, + ); + + foreach ( MWOAuthConsumerSubmitControl::$actions as $eventName ) { + // oauth-app-propose and oauth-app-update notifies admins of the app. + // oauth-app-approve, oauth-app-reject, oauth-app-disable and oauth-app-reenable + // notify owner of the change. + $notifications["oauth-app-$eventName"] = + EchoOAuthStageChangePresentationModel::getDefinition( $eventName ); + } + + $icons['oauth'] = array( 'path' => 'OAuth/frontend/assets/echo-icon.png' ); + } } diff --git a/frontend/MWOAuthUI.setup.php b/frontend/MWOAuthUI.setup.php index cf5a414..e4e00cb 100644 --- a/frontend/MWOAuthUI.setup.php +++ b/frontend/MWOAuthUI.setup.php @@ -22,6 +22,7 @@ $wgHooks['MessagesPreLoad'][] = 'MediaWiki\Extensions\OAuth\MWOAuthUIHooks::onMessagesPreLoad'; $wgHooks['SpecialPageAfterExecute'][] = 'MediaWiki\Extensions\OAuth\MWOAuthUIHooks::onSpecialPageAfterExecute'; $wgHooks['SpecialPageBeforeFormDisplay'][] = 'MediaWiki\Extensions\OAuth\MWOAuthUIHooks::onSpecialPageBeforeFormDisplay'; + $wgHooks['BeforeCreateEchoEvent'][] = 'MediaWiki\Extensions\OAuth\MWOAuthUIHooks::onBeforeCreateEchoEvent'; $wgResourceModules['ext.MWOAuth.BasicStyles'] = array( 'position' => 'top', diff --git a/frontend/assets/echo-icon.png b/frontend/assets/echo-icon.png new file mode 100644 index 0000000..675f789 --- /dev/null +++ b/frontend/assets/echo-icon.png Binary files differ diff --git a/i18n/en.json b/i18n/en.json index 7f0a75e..d6a0704 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -247,5 +247,28 @@ "action-mwoauthupdateownconsumer": "update OAuth consumers you control", "action-mwoauthviewsuppressed": "view suppressed OAuth consumers", "mwoauth-tag-reserved": "Tags beginning with <code>OAuth CID:</code> are reserved for use by OAuth.", - "mwoauth-botpasswords-note": "<strong>Note:</strong> <span class=\"plainlinks\">[$1 OAuth]</span> is more secure than bot passwords and should be preferred whenever the bot supports it." + "mwoauth-botpasswords-note": "<strong>Note:</strong> <span class=\"plainlinks\">[$1 OAuth]</span> is more secure than bot passwords and should be preferred whenever the bot supports it.", + "echo-category-title-oauth-owner": "OAuth development", + "echo-pref-tooltip-oauth-owner": "Notify me about events related to OAuth applications I have created", + "echo-category-title-oauth-admin": "OAuth admin", + "echo-pref-tooltip-oauth-admin": "Notify me about events related to reviewing OAuth applications", + "notification-oauth-app-propose-title": "$1 {{GENDER:$1|proposed}} a new OAuth app: $2", + "notification-oauth-app-update-title": "$1 {{GENDER:$2|updated}} the OAuth app $2", + "notification-oauth-app-approve-title": "$1 {{GENDER:$2|approved}} {{GENDER:$3|your}} OAuth app ($2)", + "notification-oauth-app-reject-title": "$1 {{GENDER:$2|rejected}} {{GENDER:$3|your}} OAuth app ($2)", + "notification-oauth-app-disable-title": "$1 {{GENDER:$2|disabled}} {{GENDER:$3|your}} OAuth app ($2)", + "notification-oauth-app-reenable-title": "$1 {{GENDER:$2|reenabled}} {{GENDER:$3|your}} OAuth app ($2)", + "notification-oauth-app-propose-subject": "$1 {{GENDER:$1|proposed}} a new OAuth app on {{SITENAME}}", + "notification-oauth-app-update-subject": "$1 {{GENDER:$2|updated}} an OAuth app on {{SITENAME}}", + "notification-oauth-app-approve-subject": "$1 {{GENDER:$2|approved}} {{GENDER:$3|your}} OAuth app on {{SITENAME}}", + "notification-oauth-app-reject-subject": "$1 {{GENDER:$2|rejected}} {{GENDER:$3|your}} OAuth app on {{SITENAME}}", + "notification-oauth-app-disable-subject": "$1 {{GENDER:$2|disabled}} {{GENDER:$3|your}} OAuth app on {{SITENAME}}", + "notification-oauth-app-reenable-subject": "$1 {{GENDER:$2|reenabled}} {{GENDER:$3|your}} OAuth app on {{SITENAME}}", + "notification-oauth-app-propose-primary-link": "Review app", + "notification-oauth-app-update-primary-link": "Review app", + "notification-oauth-app-approve-primary-link": "View app", + "notification-oauth-app-reject-primary-link": "View app", + "notification-oauth-app-disable-primary-link": "View app", + "notification-oauth-app-reenable-primary-link": "View app", + "notification-oauth-app-body": "Reason: $1" } diff --git a/i18n/qqq.json b/i18n/qqq.json index 49f909a..fc35d71 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -255,5 +255,28 @@ "action-mwoauthupdateownconsumer": "{{Doc-action|mwoauthupdateownconsumer}}", "action-mwoauthviewsuppressed": "{{Doc-action|mwoauthviewsuppressed}}", "mwoauth-tag-reserved": "Error message displayed on [[Special:Tags]] when a user attempts to manually create a change tag reserved by OAuth.", - "mwoauth-botpasswords-note": "Added to the top of [[Special:BotPasswords]] to point out that OAuth is a superior alternative. Parameters:\n* $1 - URL for [[Special:OAuthConsumerRegistration]], which might be on another wiki" + "mwoauth-botpasswords-note": "Added to the top of [[Special:BotPasswords]] to point out that OAuth is a superior alternative. Parameters:\n* $1 - URL for [[Special:OAuthConsumerRegistration]], which might be on another wiki", + "echo-category-title-oauth-owner": "{{doc-echo-category-title}}", + "echo-pref-tooltip-oauth-owner": "{{doc-echo-pref-tooltip}}", + "echo-category-title-oauth-admin": "{{doc-echo-category-title}}", + "echo-pref-tooltip-oauth-admin": "{{doc-echo-pref-tooltip}}", + "notification-oauth-app-propose-title": "Text of the notification sent to OAuth admins when a new app is proposed.\n* $1: app owner\n* $2: name of the app", + "notification-oauth-app-update-title": "Text of the notification sent to OAuth admins when the app is updated.\n* $1: app owner\n* $2: name of the app", + "notification-oauth-app-approve-title": "Text of the notification sent to OAuth app owner when the app is approved.\n* $1: approving admin\n* $2: name of the app\n* $3: app owner", + "notification-oauth-app-reject-title": "Text of the notification sent to OAuth app owner when the app is rejected.\n* $1: rejecting admin\n* $2: name of the app\n* $3: app owner", + "notification-oauth-app-disable-title": "Text of the notification sent to OAuth app owner when the app is disabled.\n* $1: disabling admin\n* $2: name of the app\n* $3: app owner", + "notification-oauth-app-reenable-title": "Text of the notification sent to OAuth app owner when the app is reenabled.\n* $1: reenabling admin\n* $2: name of the app\n* $3: app owner", + "notification-oauth-app-propose-subject": "Email subject of the notification sent to OAuth admins when a new app is proposed.\n* $1: app owner\n* $2: name of the app\n\nSee also: {{msg-mw|notification-oauth-app-propose-email-batch-body}}", + "notification-oauth-app-update-subject": "Email subject of the notification sent to OAuth admins when the app is updated.\n* $1: app owner\n* $2: name of the app\n\nSee also: {{msg-mw|notification-oauth-app-update-email-batch-body}}", + "notification-oauth-app-approve-subject": "Email subject of the notification sent to OAuth app owner when the app is approved.\n* $1: approving admin\n* $2: name of the app\n* $3: app owner\n\nSee also: {{msg-mw|notification-oauth-app-approve-email-batch-body}}", + "notification-oauth-app-reject-subject": "Email subject of the notification sent to OAuth app owner when the app is rejected.\n* $1: rejecting admin\n* $2: name of the app\n* $3: app owner\n\nSee also: {{msg-mw|notification-oauth-app-reject-email-batch-body}}", + "notification-oauth-app-disable-subject": "Email subject of the notification sent to OAuth app owner when the app is disabled.\n* $1: disabling admin\n* $2: name of the app\n* $3: app owner\n\nSee also: {{msg-mw|notification-oauth-app-disable-email-batch-body}}", + "notification-oauth-app-reenable-subject": "Email subject of the notification sent to OAuth app owner when the app is reenabled.\n* $1: reenabling admin\n* $2: name of the app\n* $3: app owner\n\nSee also: {{msg-mw|notification-oauth-app-reenable-email-batch-body}}", + "notification-oauth-app-propose-primary-link": "Text of the link which appears at the end of the email notification {{msg-mw|notification-oauth-app-propose-email-batch-body}}. It links to the app management page for OAuth admins.", + "notification-oauth-app-update-primary-link": "Text of the link which appears at the end of the email notification {{msg-mw|notification-oauth-app-update-email-batch-body}}. It links to the app management page for OAuth admins.", + "notification-oauth-app-approve-primary-link": "Text of the link which appears at the end of the email notification {{msg-mw|notification-oauth-app-approve-email-batch-body}}. It links to the app update page for app owners.", + "notification-oauth-app-reject-primary-link": "Text of the link which appears at the end of the email notification {{msg-mw|notification-oauth-app-reject-email-batch-body}}. It links to the app update page for app owners.", + "notification-oauth-app-disable-primary-link": "Text of the link which appears at the end of the email notification {{msg-mw|notification-oauth-app-disable-email-batch-body}}. It links to the app update page for app owners.", + "notification-oauth-app-reenable-primary-link": "Text of the link which appears at the end of the email notification {{msg-mw|notification-oauth-app-reenable-email-batch-body}}. It links to the app update page for app owners.", + "notification-oauth-app-body": "Text of the body of app stage change notifications. $1 is the reason given by the user who made the change." } -- To view, visit https://gerrit.wikimedia.org/r/231989 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Iee6e0074d648236b01ae40da704a27cfb94d4748 Gerrit-PatchSet: 11 Gerrit-Project: mediawiki/extensions/OAuth Gerrit-Branch: master Gerrit-Owner: Gergő Tisza <gti...@wikimedia.org> Gerrit-Reviewer: Anomie <bjor...@wikimedia.org> Gerrit-Reviewer: Brian Wolff <bawolff...@gmail.com> Gerrit-Reviewer: Dpatrick <dpatr...@wikimedia.org> Gerrit-Reviewer: Gergő Tisza <gti...@wikimedia.org> Gerrit-Reviewer: Legoktm <legoktm.wikipe...@gmail.com> Gerrit-Reviewer: Siebrand <siebr...@kitano.nl> Gerrit-Reviewer: Springle <sprin...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits