Users can now specify a blacklist of users who are prevented from sending them a direct email.
Bug: T138166
Change-Id: Ifa26153f593b0ca3a9121e1e29961911c616c9e4
** This is currently gated by $wgCommentTableSchemaMigrationStage. Most wikis
can set this to MIGRATION_NEW and run maintenance/migrateComments.php as
soon as any necessary extensions are updated.
+* (T138166) Added ability for users to prohibit other users from sending them
+ emails with Special:Emailuser. Can be enabled by setting
+ $wgEnableUserEmailBlacklist to true.
=== External library changes in 1.30 ===
* wfUsePHP() is deprecated.
* wfFixSessionID() was removed.
* wfShellExec() and related functions are deprecated, use Shell::command().
+* (T138166) SpecialEmailUser::getTarget() now requires a second argument, the sending
+ user object. Using the method without the second argument is deprecated.
== Compatibility ==
MediaWiki 1.30 requires PHP 5.5.9 or later. There is experimental support for
*/
$wgEnableUserEmail = true;
+/**
+ * Set to true to enable user-to-user e-mail blacklist.
+ *
+ * @since 1.30
+ */
+$wgEnableUserEmailBlacklist = false;
+
/**
* If true put the sending user's email in a Reply-To header
* instead of From (false). ($wgPasswordSender will be used as From.)
'label-message' => 'tog-ccmeonemails',
'disabled' => $disableEmailPrefs,
];
+
+ if ( $config->get( 'EnableUserEmailBlacklist' )
+ && !$disableEmailPrefs
+ && !(bool)$user->getOption( 'disablemail' )
+ ) {
+ $lookup = CentralIdLookup::factory();
+ $ids = $user->getOption( 'email-blacklist', [] );
+ $names = $ids ? $lookup->namesFromCentralIds( $ids, $user ) : [];
+
+ $defaultPreferences['email-blacklist'] = [
+ 'type' => 'usersmultiselect',
+ 'label-message' => 'email-blacklist-label',
+ 'section' => 'personal/email',
+ 'default' => implode( "\n", $names ),
+ ];
+ }
}
if ( $config->get( 'EnotifWatchlist' ) ) {
$params = $this->extractRequestParams();
// Validate target
- $targetUser = SpecialEmailUser::getTarget( $params['target'] );
+ $targetUser = SpecialEmailUser::getTarget( $params['target'], $this->getUser() );
if ( !( $targetUser instanceof User ) ) {
switch ( $targetUser ) {
case 'notarget':
$targetUser = User::newFromId( $id );
}
- # The sending user must have a confirmed email address and the target
- # user must have a confirmed email address and allow emails from users.
- return $this->getUser()->canSendEmail() &&
- $targetUser->canReceiveEmail();
+ # The sending user must have a confirmed email address and the receiving
+ # user must accept emails from the sender.
+ return $this->getUser()->canSendEmail()
+ && SpecialEmailUser::validateTarget( $targetUser, $this->getUser() ) === '';
}
/**
}
public function getDescription() {
- $target = self::getTarget( $this->mTarget );
+ $target = self::getTarget( $this->mTarget, $this->getUser() );
if ( !$target instanceof User ) {
return $this->msg( 'emailuser-title-notarget' )->text();
}
throw new ErrorPageError( $title, $msg, $params );
}
// Got a valid target user name? Else ask for one.
- $ret = self::getTarget( $this->mTarget );
+ $ret = self::getTarget( $this->mTarget, $this->getUser() );
if ( !$ret instanceof User ) {
if ( $this->mTarget != '' ) {
// Messages used here: notargettext, noemailtext, nowikiemailtext
* Validate target User
*
* @param string $target Target user name
- * @return User User object on success or a string on error
+ * @param User|null $sender User sending the email
+ * @return User|string User object on success or a string on error
*/
- public static function getTarget( $target ) {
+ public static function getTarget( $target, User $sender = null ) {
+ if ( $sender === null ) {
+ wfDeprecated( __METHOD__ . ' without specifying the sending user', '1.30' );
+ }
+
if ( $target == '' ) {
wfDebug( "Target is empty.\n" );
}
$nu = User::newFromName( $target );
- if ( !$nu instanceof User || !$nu->getId() ) {
+ $error = self::validateTarget( $nu, $sender );
+
+ return $error ? $error : $nu;
+ }
+
+ /**
+ * Validate target User
+ *
+ * @param User $target Target user
+ * @param User|null $sender User sending the email
+ * @return string Error message or empty string if valid.
+ * @since 1.30
+ */
+ public static function validateTarget( $target, User $sender = null ) {
+ if ( $sender === null ) {
+ wfDeprecated( __METHOD__ . ' without specifying the sending user', '1.30' );
+ }
+
+ if ( !$target instanceof User || !$target->getId() ) {
wfDebug( "Target is invalid user.\n" );
return 'notarget';
- } elseif ( !$nu->isEmailConfirmed() ) {
+ } elseif ( !$target->isEmailConfirmed() ) {
wfDebug( "User has no valid email.\n" );
return 'noemail';
- } elseif ( !$nu->canReceiveEmail() ) {
+ } elseif ( !$target->canReceiveEmail() ) {
wfDebug( "User does not allow user emails.\n" );
return 'nowikiemail';
+ } elseif ( $sender !== null ) {
+ $blacklist = $target->getOption( 'email-blacklist', [] );
+ if ( $blacklist ) {
+ $lookup = CentralIdLookup::factory();
+ $senderId = $lookup->centralIdFromLocalUser( $sender );
+ if ( $senderId !== 0 && in_array( $senderId, $blacklist ) ) {
+ wfDebug( "User does not allow user emails from this user.\n" );
+
+ return 'nowikiemail';
+ }
+ }
}
- return $nu;
+ return '';
}
/**
public static function submit( array $data, IContextSource $context ) {
$config = $context->getConfig();
- $target = self::getTarget( $data['Target'] );
+ $target = self::getTarget( $data['Target'], $context->getUser() );
if ( !$target instanceof User ) {
// Messages used here: notargettext, noemailtext, nowikiemailtext
return Status::newFatal( $target . 'text' );
return $idToName[$id];
}
+ /**
+ * Given a an array of central user IDs, return the (local) user names.
+ * @param int[] $ids Central user IDs
+ * @param int|User $audience One of the audience constants, or a specific user
+ * @param int $flags IDBAccessObject read flags
+ * @return string[] User names
+ * @since 1.30
+ */
+ public function namesFromCentralIds(
+ array $ids, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+ ) {
+ $idToName = array_fill_keys( $ids, false );
+ $names = $this->lookupCentralIds( $idToName, $audience, $flags );
+ $names = array_unique( $names );
+ $names = array_filter( $names, function ( $name ) {
+ return $name !== false && $name !== '';
+ } );
+
+ return array_values( $names );
+ }
+
/**
* Given a (local) user name, return the central ID
* @note There's no requirement that the user name actually exists locally,
return $nameToId[$name];
}
+ /**
+ * Given an array of (local) user names, return the central IDs.
+ * @param string[] $names Canonicalized user names
+ * @param int|User $audience One of the audience constants, or a specific user
+ * @param int $flags IDBAccessObject read flags
+ * @return int[] User IDs
+ * @since 1.30
+ */
+ public function centralIdsFromNames(
+ array $names, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL
+ ) {
+ $nameToId = array_fill_keys( $names, false );
+ $ids = $this->lookupUserNames( $nameToId, $audience, $flags );
+ $ids = array_unique( $ids );
+ $ids = array_filter( $ids, function ( $id ) {
+ return $id !== false;
+ } );
+
+ return array_values( $ids );
+ }
+
/**
* Given a central user ID, return a local User object
* @note Unlike nameFromCentralId(), this does guarantee that the local
$data[$row->up_property] = $row->up_value;
}
}
+
+ // Convert the email blacklist from a new line delimited string
+ // to an array of ids.
+ if ( isset( $data['email-blacklist'] ) && $data['email-blacklist'] ) {
+ $data['email-blacklist'] = array_map( 'intval', explode( "\n", $data['email-blacklist'] ) );
+ }
+
foreach ( $data as $property => $value ) {
$this->mOptionOverrides[$property] = $value;
$this->mOptions[$property] = $value;
// Not using getOptions(), to keep hidden preferences in database
$saveOptions = $this->mOptions;
+ // Convert usernames to ids.
+ if ( isset( $this->mOptions['email-blacklist'] ) ) {
+ if ( $this->mOptions['email-blacklist'] ) {
+ $value = $this->mOptions['email-blacklist'];
+ // Email Blacklist may be an array of ids or a string of new line
+ // delimnated user names.
+ if ( is_array( $value ) ) {
+ $ids = array_filter( $value, 'is_numeric' );
+ } else {
+ $lookup = CentralIdLookup::factory();
+ $ids = $lookup->centralIdsFromNames( explode( "\n", $value ), $this );
+ }
+ $this->mOptions['email-blacklist'] = $ids;
+ $saveOptions['email-blacklist'] = implode( "\n", $this->mOptions['email-blacklist'] );
+ } else {
+ // If the blacklist is empty, set it to null rather than an empty string.
+ $this->mOptions['email-blacklist'] = null;
+ }
+ }
+
// Allow hooks to abort, for instance to save to a global profile.
// Reset options to default state before saving.
if ( !Hooks::run( 'UserSaveOptions', [ $this, &$saveOptions ] ) ) {
"timezoneregion-indian": "Indian Ocean",
"timezoneregion-pacific": "Pacific Ocean",
"allowemail": "Enable email from other users",
+ "email-blacklist-label": "Prohibit these users from sending emails to me:",
"prefs-searchoptions": "Search",
"prefs-namespaces": "Namespaces",
"default": "default",
"timezoneregion-indian": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"timezoneregion-pacific": "Used in \"Time zone\" listbox in [[Special:Preferences#mw-prefsection-datetime|preferences]], \"date and time\" tab.\n{{Related|Timezoneregion}}",
"allowemail": "Used in [[Special:Preferences]] > {{int:prefs-personal}} > {{int:email}}.",
+ "email-blacklist-label": "Used in [[Special:Preferences]] > {{int:prefs-prohibit}} > {{int:email}}.",
"prefs-searchoptions": "{{Identical|Search}}",
"prefs-namespaces": "Shown as legend of the second fieldset of the tab 'Search' in [[Special:Preferences]]\n{{Identical|Namespace}}",
"default": "{{Identical|Default}}",