From ed13d3fa56c2ec3a9e823161afab888937cc26c5 Mon Sep 17 00:00:00 2001 From: Dayllan Maza Date: Tue, 7 May 2019 10:55:48 -0400 Subject: [PATCH] Add Special:Mute as a shortcut for muting notifications - Special:Mute has been added as a quick way for users to block unwanted emails from other users originating from Special:EmailUser - Special:Mute can be enabled by setting $wgEnableSpecialMute = true. This flag default value is `false`. This flag is temporary until enough wikis have this feature enabled and then it will go away - When Special:Mute is enabled, emails sent from Special:EmailUser contain a link to Special:Mute to facilitate access to the page In the future, Special:Mute will support extensions to append other types of notifications that could be muted. These are some of the tasks tracking the rest of the work: - T218270 - T220163 - T218266 It is worth mentioning that blocking emails from users is already available via Special:Preferences Bug: T218265 Change-Id: I54b847192f42ee1f37999d36c3a187f8826f55a8 --- RELEASE-NOTES-1.34 | 6 +- autoload.php | 1 + includes/DefaultSettings.php | 10 + includes/specialpage/SpecialPageFactory.php | 6 + includes/specials/SpecialEmailUser.php | 9 + includes/specials/SpecialMute.php | 213 ++++++++++++++++++ includes/specials/helpers/LoginHelper.php | 1 + languages/i18n/en.json | 10 + languages/i18n/qqq.json | 10 + languages/messages/MessagesEn.php | 1 + resources/Resources.php | 5 +- resources/src/mediawiki.special.mute.js | 23 ++ .../src/mediawiki.special.pageLanguage.js | 8 +- .../includes/specials/SpecialMuteTest.php | 110 +++++++++ 14 files changed, 408 insertions(+), 5 deletions(-) create mode 100644 includes/specials/SpecialMute.php create mode 100644 resources/src/mediawiki.special.mute.js create mode 100644 tests/phpunit/includes/specials/SpecialMuteTest.php diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 33d060d477..c85a14c808 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -33,6 +33,9 @@ For notes on 1.33.x and older releases, see HISTORY. the code as the request identificator. Otherwise, the sent header will be ignored and the request ID will either be taken from Apache's mod_unique module or will be generated by Mediawiki itself (depending on the set-up). +* $wgEnableSpecialMute (T218265) - This configuration controls whether + Special:Mute is available and whether to include a link to it on emails + originating from Special:Email. ==== Changed configuration ==== * $wgUseCdn, $wgCdnServers, $wgCdnServersNoPurge, and $wgCdnMaxAge – These four @@ -57,7 +60,8 @@ For notes on 1.33.x and older releases, see HISTORY. wikidiff2.moved_paragraph_detection_cutoff. === New user-facing features in 1.34 === -* … +* Special:Mute has been added as a quick way for users to block unwanted emails + from other users originating from Special:EmailUser. === New developer features in 1.34 === * Language::formatTimePeriod now supports the new 'avoidhours' option to output diff --git a/autoload.php b/autoload.php index b26549e137..5d3adc8881 100644 --- a/autoload.php +++ b/autoload.php @@ -1390,6 +1390,7 @@ $wgAutoloadLocalClasses = [ 'SpecialLockdb' => __DIR__ . '/includes/specials/SpecialLockdb.php', 'SpecialLog' => __DIR__ . '/includes/specials/SpecialLog.php', 'SpecialMergeHistory' => __DIR__ . '/includes/specials/SpecialMergeHistory.php', + 'SpecialMute' => __DIR__ . '/includes/specials/SpecialMute.php', 'SpecialMyLanguage' => __DIR__ . '/includes/specials/SpecialMyLanguage.php', 'SpecialMycontributions' => __DIR__ . '/includes/specials/redirects/SpecialMycontributions.php', 'SpecialMypage' => __DIR__ . '/includes/specials/redirects/SpecialMypage.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 73d05ff408..96d9726a11 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1682,6 +1682,16 @@ $wgEnableEmail = true; */ $wgEnableUserEmail = true; +/** + * Set to true to enable the Special Mute page. This allows users + * to mute unwanted communications from other users, and is linked + * to from emails originating from Special:Email. + * + * @since 1.34 + * @deprecated 1.34 + */ +$wgEnableSpecialMute = false; + /** * Set to true to enable user-to-user e-mail blacklist. * diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index 1053bda5c4..94900d46de 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -232,6 +232,7 @@ class SpecialPageFactory { 'EmailAuthentication', 'EnableEmail', 'EnableJavaScriptTest', + 'EnableSpecialMute', 'PageLanguageUseDB', 'SpecialPages', ]; @@ -282,9 +283,14 @@ class SpecialPageFactory { $this->list['JavaScriptTest'] = \SpecialJavaScriptTest::class; } + if ( $this->options->get( 'EnableSpecialMute' ) ) { + $this->list['Mute'] = \SpecialMute::class; + } + if ( $this->options->get( 'PageLanguageUseDB' ) ) { $this->list['PageLanguage'] = \SpecialPageLanguage::class; } + if ( $this->options->get( 'ContentHandlerUseDB' ) ) { $this->list['ChangeContentModel'] = \SpecialChangeContentModel::class; } diff --git a/includes/specials/SpecialEmailUser.php b/includes/specials/SpecialEmailUser.php index 5f80215632..5b07f22453 100644 --- a/includes/specials/SpecialEmailUser.php +++ b/includes/specials/SpecialEmailUser.php @@ -387,6 +387,15 @@ class SpecialEmailUser extends UnlistedSpecialPage { $text .= $context->msg( 'emailuserfooter', $from->name, $to->name )->inContentLanguage()->text(); + if ( $config->get( 'EnableSpecialMute' ) ) { + $specialMutePage = SpecialPage::getTitleFor( 'Mute', $context->getUser()->getName() ); + $text .= "\n" . $context->msg( + 'specialmute-email-footer', + $specialMutePage->getCanonicalURL(), + $context->getUser()->getName() + ); + } + // Check and increment the rate limits if ( $context->getUser()->pingLimiter( 'emailuser' ) ) { throw new ThrottledError(); diff --git a/includes/specials/SpecialMute.php b/includes/specials/SpecialMute.php new file mode 100644 index 0000000000..4f34785115 --- /dev/null +++ b/includes/specials/SpecialMute.php @@ -0,0 +1,213 @@ +getConfig(); + $this->enableUserEmailBlacklist = $config->get( 'EnableUserEmailBlacklist' ); + $this->enableUserEmail = $config->get( 'EnableUserEmail' ); + + $this->centralIdLookup = CentralIdLookup::factory(); + + parent::__construct( 'Mute', '', false ); + } + + /** + * Entry point for special pages + * + * @param string $par + */ + public function execute( $par ) { + $this->requireLogin( 'specialmute-login-required' ); + $this->loadTarget( $par ); + + parent::execute( $par ); + + $out = $this->getOutput(); + $out->addModules( 'mediawiki.special.pageLanguage' ); + } + + /** + * @inheritDoc + */ + public function requiresUnblock() { + return false; + } + + /** + * @inheritDoc + */ + protected function getDisplayFormat() { + return 'ooui'; + } + + /** + * @inheritDoc + */ + public function onSuccess() { + $out = $this->getOutput(); + $out->addWikiMsg( 'specialmute-success' ); + } + + /** + * @param array $data + * @param HTMLForm|null $form + * @return bool + */ + public function onSubmit( array $data, HTMLForm $form = null ) { + if ( !empty( $data['MuteEmail'] ) ) { + $this->muteEmailsFromTarget(); + } else { + $this->unmuteEmailsFromTarget(); + } + + return true; + } + + /** + * @inheritDoc + */ + public function getDescription() { + return $this->msg( 'specialmute' )->text(); + } + + /** + * Un-mute emails from target + */ + private function unmuteEmailsFromTarget() { + $blacklist = $this->getBlacklist(); + + $key = array_search( $this->targetCentralId, $blacklist ); + if ( $key !== false ) { + unset( $blacklist[$key] ); + $blacklist = implode( "\n", $blacklist ); + + $user = $this->getUser(); + $user->setOption( 'email-blacklist', $blacklist ); + $user->saveSettings(); + } + } + + /** + * Mute emails from target + */ + private function muteEmailsFromTarget() { + // avoid duplicates just in case + if ( !$this->isTargetBlacklisted() ) { + $blacklist = $this->getBlacklist(); + + $blacklist[] = $this->targetCentralId; + $blacklist = implode( "\n", $blacklist ); + + $user = $this->getUser(); + $user->setOption( 'email-blacklist', $blacklist ); + $user->saveSettings(); + } + } + + /** + * @inheritDoc + */ + protected function alterForm( HTMLForm $form ) { + $form->setId( 'mw-specialmute-form' ); + $form->setHeaderText( $this->msg( 'specialmute-header', $this->target )->parse() ); + $form->setSubmitTextMsg( 'specialmute-submit' ); + $form->setSubmitID( 'save' ); + } + + /** + * @inheritDoc + */ + protected function getFormFields() { + if ( !$this->enableUserEmailBlacklist || !$this->enableUserEmail ) { + throw new ErrorPageError( 'specialmute', 'specialmute-error-email-blacklist-disabled' ); + } + + if ( !$this->getUser()->getEmailAuthenticationTimestamp() ) { + throw new ErrorPageError( 'specialmute', 'specialmute-error-email-preferences' ); + } + + $fields['MuteEmail'] = [ + 'type' => 'check', + 'label-message' => 'specialmute-label-mute-email', + 'default' => $this->isTargetBlacklisted(), + ]; + + return $fields; + } + + /** + * @param string $username + */ + private function loadTarget( $username ) { + $target = User::newFromName( $username ); + if ( !$target || !$target->getId() ) { + throw new ErrorPageError( 'specialmute', 'specialmute-error-invalid-user' ); + } else { + $this->target = $target; + $this->targetCentralId = $this->centralIdLookup->centralIdFromLocalUser( $target ); + } + } + + /** + * @return bool + */ + private function isTargetBlacklisted() { + $blacklist = $this->getBlacklist(); + return in_array( $this->targetCentralId, $blacklist ); + } + + /** + * @return array + */ + private function getBlacklist() { + $blacklist = $this->getUser()->getOption( 'email-blacklist' ); + if ( !$blacklist ) { + return []; + } + + return MultiUsernameFilter::splitIds( $blacklist ); + } +} diff --git a/includes/specials/helpers/LoginHelper.php b/includes/specials/helpers/LoginHelper.php index 6c9bea598f..f66eccf7bc 100644 --- a/includes/specials/helpers/LoginHelper.php +++ b/includes/specials/helpers/LoginHelper.php @@ -25,6 +25,7 @@ class LoginHelper extends ContextSource { 'resetpass-no-info', 'confirmemail_needlogin', 'prefsnologintext2', + 'specialmute-login-required', ]; /** diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 851a6b2c50..a9bf0659d5 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4194,6 +4194,16 @@ "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use:
0.0.0.0/0\n::/0
", "edit-error-short": "Error: $1", "edit-error-long": "Errors:\n\n$1", + "specialmute": "Mute", + "specialmute-success": "Your mute preferences have been successfully updated. See all muted users in [[Special:Preferences]].", + "specialmute-submit": "Confirm", + "specialmute-label-mute-email": "Mute emails from this user", + "specialmute-header": "Please select your mute preferences for [[User:$1]].", + "specialmute-error-invalid-user": "The username requested could not be found.", + "specialmute-error-email-blacklist-disabled": "Muting users from sending you emails is not enabled.", + "specialmute-error-email-preferences": "You must confirm your email address before you can mute a user. You may do so from [[Special:Preferences]].", + "specialmute-email-footer": "[$1 Manage email preferences for $2.]", + "specialmute-login-required": "Please log in to change your mute preferences.", "revid": "revision $1", "pageid": "page ID $1", "interfaceadmin-info": "$1\n\nPermissions for editing of sitewide CSS/JS/JSON files were recently separated from the editinterface right. If you do not understand why you are getting this error, see [[mw:MediaWiki_1.32/interface-admin]].", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index e0da190bb7..49c33f218e 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4402,6 +4402,16 @@ "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).", "edit-error-short": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-long}}\n{{Identical|Error}}", "edit-error-long": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-short}}\n{{Identical|Error}}", + "specialmute": "The name of the special page [[Special:Mute]].", + "specialmute-success": "The content of [[Special:Mute]] with a successful message indicating that your mute preferences have been updated after the form has been submitted.", + "specialmute-submit": "Submit button on [[Special:Mute]] form.", + "specialmute-label-mute-email": "Label for the checkbox that mutes/unmutes emails from the specified user.", + "specialmute-header": "Used as header text on [[Special:Mute]]. Shown before the form with the muting options.\n* $1 - User selected for muting", + "specialmute-error-invalid-user": "Error displayed when the username cannot be found.", + "specialmute-error-email-blacklist-disabled": "Error displayed when email blacklist is not enabled.", + "specialmute-error-email-preferences": "Error displayed when the user has not confirmed their email address.", + "specialmute-email-footer": "Email footer linking to [[Special:Mute]] preselecting the sender to manage muting options.\n* $1 - Url linking to [[Special:Mute]].\n* $2 - The user sending the email.", + "specialmute-login-required": "Error displayed when a user tries to access [[Special:Mute]] before logging in.", "revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.\n{{Identical|Revision}}", "pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number.", "interfaceadmin-info": "Part of the error message shown when someone with the editinterface right but without the appropriate editsite* right tries to edit a sitewide CSS/JSON/JS page.", diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 666b28f82e..22313a439f 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -447,6 +447,7 @@ $specialPageAliases = [ 'Mostlinkedtemplates' => [ 'MostTranscludedPages', 'MostLinkedTemplates', 'MostUsedTemplates' ], 'Mostrevisions' => [ 'MostRevisions' ], 'Movepage' => [ 'MovePage' ], + 'Mute' => [ 'Mute' ], 'Mycontributions' => [ 'MyContributions' ], 'MyLanguage' => [ 'MyLanguage' ], 'Mypage' => [ 'MyPage' ], diff --git a/resources/Resources.php b/resources/Resources.php index b90ead4c45..9c26986afe 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2150,7 +2150,10 @@ return [ ], ], 'mediawiki.special.pageLanguage' => [ - 'scripts' => 'resources/src/mediawiki.special.pageLanguage.js', + 'scripts' => [ + 'resources/src/mediawiki.special.mute.js', + 'resources/src/mediawiki.special.pageLanguage.js' + ], 'dependencies' => [ 'oojs-ui-core', ], diff --git a/resources/src/mediawiki.special.mute.js b/resources/src/mediawiki.special.mute.js new file mode 100644 index 0000000000..3d494d02ea --- /dev/null +++ b/resources/src/mediawiki.special.mute.js @@ -0,0 +1,23 @@ +( function () { + 'use strict'; + + $( function () { + var $inputs = $( '#mw-specialmute-form input:checkbox' ), + saveButton, $saveButton = $( '#save' ); + + function isFormChanged() { + return $inputs.is( function () { + return this.checked !== this.defaultChecked; + } ); + } + + if ( $saveButton.length ) { + saveButton = OO.ui.infuse( $saveButton ); + saveButton.setDisabled( !isFormChanged() ); + + $inputs.on( 'change', function () { + saveButton.setDisabled( !isFormChanged() ); + } ); + } + } ); +}() ); diff --git a/resources/src/mediawiki.special.pageLanguage.js b/resources/src/mediawiki.special.pageLanguage.js index 8b70e1fd0b..8538e955b5 100644 --- a/resources/src/mediawiki.special.pageLanguage.js +++ b/resources/src/mediawiki.special.pageLanguage.js @@ -4,8 +4,10 @@ ( function () { $( function () { // Select the 'Language select' option if user is trying to select language - OO.ui.infuse( $( '#mw-pl-languageselector' ) ).on( 'change', function () { - OO.ui.infuse( $( '#mw-pl-options' ) ).setValue( '2' ); - } ); + if ( $( '#mw-pl-languageselector' ).length ) { + OO.ui.infuse( $( '#mw-pl-languageselector' ) ).on( 'change', function () { + OO.ui.infuse( $( '#mw-pl-options' ) ).setValue( '2' ); + } ); + } } ); }() ); diff --git a/tests/phpunit/includes/specials/SpecialMuteTest.php b/tests/phpunit/includes/specials/SpecialMuteTest.php new file mode 100644 index 0000000000..e31357cb33 --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialMuteTest.php @@ -0,0 +1,110 @@ +setMwGlobals( [ + 'wgEnableUserEmailBlacklist' => true + ] ); + } + + /** + * @inheritDoc + */ + protected function newSpecialPage() { + return new SpecialMute(); + } + + /** + * @covers SpecialMute::execute + * @expectedExceptionMessage username requested could not be found + * @expectedException ErrorPageError + */ + public function testInvalidTarget() { + $user = $this->getTestUser()->getUser(); + $this->executeSpecialPage( + 'InvalidUser', null, 'qqx', $user + ); + } + + /** + * @covers SpecialMute::execute + * @expectedExceptionMessage Muting users from sending you emails is not enabled + * @expectedException ErrorPageError + */ + public function testEmailBlacklistNotEnabled() { + $this->setMwGlobals( [ + 'wgEnableUserEmailBlacklist' => false + ] ); + + $user = $this->getTestUser()->getUser(); + $this->executeSpecialPage( + $user->getName(), null, 'qqx', $user + ); + } + + /** + * @covers SpecialMute::execute + * @expectedException UserNotLoggedIn + */ + public function testUserNotLoggedIn() { + $this->executeSpecialPage( 'TestUser' ); + } + + /** + * @covers SpecialMute::execute + */ + public function testMuteAddsUserToEmailBlacklist() { + $this->setMwGlobals( [ + 'wgCentralIdLookupProvider' => 'local', + ] ); + + $targetUser = $this->getTestUser()->getUser(); + + $loggedInUser = $this->getMutableTestUser()->getUser(); + $loggedInUser->setOption( 'email-blacklist', "999" ); + $loggedInUser->confirmEmail(); + $loggedInUser->saveSettings(); + + $fauxRequest = new FauxRequest( [ 'wpMuteEmail' => 1 ], true ); + list( $html, ) = $this->executeSpecialPage( + $targetUser->getName(), $fauxRequest, 'qqx', $loggedInUser + ); + + $this->assertContains( 'specialmute-success', $html ); + $this->assertEquals( + "999\n" . $targetUser->getId(), + $loggedInUser->getOption( 'email-blacklist' ) + ); + } + + /** + * @covers SpecialMute::execute + */ + public function testUnmuteRemovesUserFromEmailBlacklist() { + $this->setMwGlobals( [ + 'wgCentralIdLookupProvider' => 'local', + ] ); + + $targetUser = $this->getTestUser()->getUser(); + + $loggedInUser = $this->getMutableTestUser()->getUser(); + $loggedInUser->setOption( 'email-blacklist', "999\n" . $targetUser->getId() ); + $loggedInUser->confirmEmail(); + $loggedInUser->saveSettings(); + + $fauxRequest = new FauxRequest( [ 'wpMuteEmail' => false ], true ); + list( $html, ) = $this->executeSpecialPage( + $targetUser->getName(), $fauxRequest, 'qqx', $loggedInUser + ); + + $this->assertContains( 'specialmute-success', $html ); + $this->assertEquals( "999", $loggedInUser->getOption( 'email-blacklist' ) ); + } +} -- 2.20.1