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
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
'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',
*/
$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.
*
'EmailAuthentication',
'EnableEmail',
'EnableJavaScriptTest',
+ 'EnableSpecialMute',
'PageLanguageUseDB',
'SpecialPages',
];
$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;
}
$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();
--- /dev/null
+<?php
+/*
+ * 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.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+use MediaWiki\Preferences\MultiUsernameFilter;
+
+/**
+ * A special page that allows users to modify their notification
+ * preferences
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialMute extends FormSpecialPage {
+
+ /** @var User */
+ private $target;
+
+ /** @var int */
+ private $targetCentralId;
+
+ /** @var bool */
+ private $enableUserEmailBlacklist;
+
+ /** @var bool */
+ private $enableUserEmail;
+
+ /** @var CentralIdLookup */
+ private $centralIdLookup;
+
+ public function __construct() {
+ // TODO: inject all these dependencies once T222388 is resolved
+ $config = RequestContext::getMain()->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 );
+ }
+}
'resetpass-no-info',
'confirmemail_needlogin',
'prefsnologintext2',
+ 'specialmute-login-required',
];
/**
"restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use:<pre>0.0.0.0/0\n::/0</pre>",
"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 <code>editinterface</code> right. If you do not understand why you are getting this error, see [[mw:MediaWiki_1.32/interface-admin]].",
"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 <code>editinterface</code> right but without the appropriate <code>editsite*</code> right tries to edit a sitewide CSS/JSON/JS page.",
'Mostlinkedtemplates' => [ 'MostTranscludedPages', 'MostLinkedTemplates', 'MostUsedTemplates' ],
'Mostrevisions' => [ 'MostRevisions' ],
'Movepage' => [ 'MovePage' ],
+ 'Mute' => [ 'Mute' ],
'Mycontributions' => [ 'MyContributions' ],
'MyLanguage' => [ 'MyLanguage' ],
'Mypage' => [ 'MyPage' ],
],
],
'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',
],
--- /dev/null
+( 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() );
+ } );
+ }
+ } );
+}() );
( 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' );
+ } );
+ }
} );
}() );
--- /dev/null
+<?php
+
+/**
+ * @group SpecialPage
+ * @covers SpecialMute
+ */
+class SpecialMuteTest extends SpecialPageTestBase {
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->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' ) );
+ }
+}