MediaWiki  master
SpecialEmailUser.php
Go to the documentation of this file.
1 <?php
30 
37  protected $mTarget;
38 
42  protected $mTargetObj;
43 
45  private $userNameUtils;
46 
48  private $userNamePrefixSearch;
49 
51  private $userOptionsLookup;
52 
58  public function __construct(
59  UserNameUtils $userNameUtils,
60  UserNamePrefixSearch $userNamePrefixSearch,
61  UserOptionsLookup $userOptionsLookup
62  ) {
63  parent::__construct( 'Emailuser' );
64  $this->userNameUtils = $userNameUtils;
65  $this->userNamePrefixSearch = $userNamePrefixSearch;
66  $this->userOptionsLookup = $userOptionsLookup;
67  }
68 
69  public function doesWrites() {
70  return true;
71  }
72 
73  public function getDescription() {
74  $target = self::getTarget( $this->mTarget, $this->getUser() );
75  if ( !$target instanceof User ) {
76  return $this->msg( 'emailuser-title-notarget' )->text();
77  }
78 
79  return $this->msg( 'emailuser-title-target', $target->getName() )->text();
80  }
81 
82  protected function getFormFields() {
83  $linkRenderer = $this->getLinkRenderer();
84  $user = $this->getUser();
85  return [
86  'From' => [
87  'type' => 'info',
88  'raw' => 1,
89  'default' => $linkRenderer->makeLink(
90  $user->getUserPage(),
91  $user->getName()
92  ),
93  'label-message' => 'emailfrom',
94  'id' => 'mw-emailuser-sender',
95  ],
96  'To' => [
97  'type' => 'info',
98  'raw' => 1,
99  'default' => $linkRenderer->makeLink(
100  $this->mTargetObj->getUserPage(),
101  $this->mTargetObj->getName()
102  ),
103  'label-message' => 'emailto',
104  'id' => 'mw-emailuser-recipient',
105  ],
106  'Target' => [
107  'type' => 'hidden',
108  'default' => $this->mTargetObj->getName(),
109  ],
110  'Subject' => [
111  'type' => 'text',
112  'default' => $this->msg( 'defemailsubject', $user->getName() )->inContentLanguage()->text(),
113  'label-message' => 'emailsubject',
114  'maxlength' => 200,
115  'size' => 60,
116  'required' => true,
117  ],
118  'Text' => [
119  'type' => 'textarea',
120  'rows' => 20,
121  'label-message' => 'emailmessage',
122  'required' => true,
123  ],
124  'CCMe' => [
125  'type' => 'check',
126  'label-message' => 'emailccme',
127  'default' => $this->userOptionsLookup->getBoolOption( $user, 'ccmeonemails' ),
128  ],
129  ];
130  }
131 
132  public function execute( $par ) {
133  $out = $this->getOutput();
134  $request = $this->getRequest();
135  $out->addModuleStyles( 'mediawiki.special' );
136 
137  $this->mTarget = $par ?? $request->getVal( 'wpTarget', $request->getVal( 'target', '' ) );
138 
139  // This needs to be below assignment of $this->mTarget because
140  // getDescription() needs it to determine the correct page title.
141  $this->setHeaders();
142  $this->outputHeader();
143 
144  // error out if sending user cannot do this
145  $error = self::getPermissionsError(
146  $this->getUser(),
147  $this->getRequest()->getVal( 'wpEditToken' ),
148  $this->getConfig()
149  );
150 
151  switch ( $error ) {
152  case null:
153  # Wahey!
154  break;
155  case 'badaccess':
156  throw new PermissionsError( 'sendemail' );
157  case 'blockedemailuser':
158  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
159  throw new UserBlockedError( $this->getUser()->getBlock() );
160  case 'actionthrottledtext':
161  throw new ThrottledError;
162  case 'mailnologin':
163  case 'usermaildisabled':
164  throw new ErrorPageError( $error, "{$error}text" );
165  default:
166  # It's a hook error
167  [ $title, $msg, $params ] = $error;
168  throw new ErrorPageError( $title, $msg, $params );
169  }
170 
171  // A little hack: HTMLForm will check $this->mTarget only, if the form was posted, not
172  // if the user opens Special:EmailUser/Florian (e.g.). So check, if the user did that
173  // and show the "Send email to user" form directly, if so. Show the "enter username"
174  // form, otherwise.
175  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable target is set
176  $this->mTargetObj = self::getTarget( $this->mTarget, $this->getUser() );
177  if ( !$this->mTargetObj instanceof User ) {
178  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable target is set
179  $this->userForm( $this->mTarget );
180  } else {
181  $this->sendEmailForm();
182  }
183  }
184 
192  public static function getTarget( $target, User $sender ) {
193  if ( $target == '' ) {
194  wfDebug( "Target is empty." );
195 
196  return 'notarget';
197  }
198 
199  $nu = User::newFromName( $target );
200  $error = self::validateTarget( $nu, $sender );
201 
202  return $error ?: $nu;
203  }
204 
213  public static function validateTarget( $target, User $sender ) {
214  if ( !$target instanceof User || !$target->getId() ) {
215  wfDebug( "Target is invalid user." );
216 
217  return 'notarget';
218  }
219 
220  if ( !$target->isEmailConfirmed() ) {
221  wfDebug( "User has no valid email." );
222 
223  return 'noemail';
224  }
225 
226  if ( !$target->canReceiveEmail() ) {
227  wfDebug( "User does not allow user emails." );
228 
229  return 'nowikiemail';
230  }
231 
232  $userOptionsLookup = MediaWikiServices::getInstance()
233  ->getUserOptionsLookup();
234  if ( !$userOptionsLookup->getOption(
235  $target,
236  'email-allow-new-users'
237  ) && $sender->isNewbie()
238  ) {
239  wfDebug( "User does not allow user emails from new users." );
240 
241  return 'nowikiemail';
242  }
243 
244  $muteList = $userOptionsLookup->getOption(
245  $target,
246  'email-blacklist',
247  ''
248  );
249  if ( $muteList ) {
250  $muteList = MultiUsernameFilter::splitIds( $muteList );
251  $senderId = MediaWikiServices::getInstance()
252  ->getCentralIdLookup()
253  ->centralIdFromLocalUser( $sender );
254  if ( $senderId !== 0 && in_array( $senderId, $muteList ) ) {
255  wfDebug( "User does not allow user emails from this user." );
256 
257  return 'nowikiemail';
258  }
259  }
260 
261  return '';
262  }
263 
273  public static function getPermissionsError( $user, $editToken, Config $config = null ) {
274  if ( $config === null ) {
275  wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
276  $config = MediaWikiServices::getInstance()->getMainConfig();
277  }
278  if ( !$config->get( MainConfigNames::EnableEmail ) ||
279  !$config->get( MainConfigNames::EnableUserEmail ) ) {
280  return 'usermaildisabled';
281  }
282 
283  // Run this before checking 'sendemail' permission
284  // to show appropriate message to anons (T160309)
285  if ( !$user->isEmailConfirmed() ) {
286  return 'mailnologin';
287  }
288 
289  if ( !MediaWikiServices::getInstance()
290  ->getPermissionManager()
291  ->userHasRight( $user, 'sendemail' )
292  ) {
293  return 'badaccess';
294  }
295 
296  if ( $user->isBlockedFromEmailuser() ) {
297  wfDebug( "User is blocked from sending e-mail." );
298 
299  return "blockedemailuser";
300  }
301 
302  // Check the ping limiter without incrementing it - we'll check it
303  // again later and increment it on a successful send
304  if ( $user->pingLimiter( 'emailuser', 0 ) ) {
305  wfDebug( "Ping limiter triggered." );
306 
307  return 'actionthrottledtext';
308  }
309 
310  $hookErr = false;
311 
312  Hooks::runner()->onUserCanSendEmail( $user, $hookErr );
313  Hooks::runner()->onEmailUserPermissionsErrors( $user, $editToken, $hookErr );
314 
315  if ( $hookErr ) {
316  return $hookErr;
317  }
318 
319  return null;
320  }
321 
327  protected function userForm( $name ) {
328  $htmlForm = HTMLForm::factory( 'ooui', [
329  'Target' => [
330  'type' => 'user',
331  'exists' => true,
332  'required' => true,
333  'label' => $this->msg( 'emailusername' )->text(),
334  'id' => 'emailusertarget',
335  'autofocus' => true,
336  'value' => $name,
337  ]
338  ], $this->getContext() );
339 
340  $htmlForm
341  ->setTitle( $this->getPageTitle() ) // Remove subpage
342  ->setSubmitCallback( [ $this, 'sendEmailForm' ] )
343  ->setFormIdentifier( 'userForm' )
344  ->setId( 'askusername' )
345  ->setWrapperLegendMsg( 'emailtarget' )
346  ->setSubmitTextMsg( 'emailusernamesubmit' )
347  ->show();
348  }
349 
350  public function sendEmailForm() {
351  $out = $this->getOutput();
352 
353  $ret = $this->mTargetObj;
354  if ( !$ret instanceof User ) {
355  if ( $this->mTarget != '' ) {
356  // Messages used here: notargettext, noemailtext, nowikiemailtext
357  $ret = ( $ret == 'notarget' ) ? 'emailnotarget' : ( $ret . 'text' );
358  return Status::newFatal( $ret );
359  }
360  return false;
361  }
362 
363  $htmlForm = HTMLForm::factory( 'ooui', $this->getFormFields(), $this->getContext() );
364  // By now we are supposed to be sure that $this->mTarget is a user name
365  $htmlForm
366  ->setTitle( $this->getPageTitle() ) // Remove subpage
367  ->addPreText( $this->msg( 'emailpagetext', $this->mTarget )->parse() )
368  ->setSubmitTextMsg( 'emailsend' )
369  ->setSubmitCallback( [ __CLASS__, 'submit' ] )
370  ->setFormIdentifier( 'sendEmailForm' )
371  ->setWrapperLegendMsg( 'email-legend' )
372  ->prepareForm();
373 
374  if ( !$this->getHookRunner()->onEmailUserForm( $htmlForm ) ) {
375  return false;
376  }
377 
378  $result = $htmlForm->show();
379 
380  if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
381  $out->setPageTitle( $this->msg( 'emailsent' ) );
382  $out->addWikiMsg( 'emailsenttext', $this->mTarget );
383  $out->returnToMain( false, $ret->getUserPage() );
384  }
385  return true;
386  }
387 
398  public static function submit( array $data, IContextSource $context ) {
399  $config = $context->getConfig();
400 
401  $sender = $context->getUser();
402  $target = self::getTarget( $data['Target'], $sender );
403  if ( !$target instanceof User ) {
404  // Messages used here: notargettext, noemailtext, nowikiemailtext
405  return Status::newFatal( $target . 'text' );
406  }
407 
408  $toAddress = MailAddress::newFromUser( $target );
409  $fromAddress = MailAddress::newFromUser( $sender );
410  $subject = $data['Subject'];
411  $text = $data['Text'];
412 
413  // Add a standard footer and trim up trailing newlines
414  $text = rtrim( $text ) . "\n\n-- \n";
415  $text .= $context->msg(
416  'emailuserfooter',
417  $fromAddress->name,
418  $toAddress->name
419  )->inContentLanguage()->text();
420 
421  if ( $config->get( MainConfigNames::EnableSpecialMute ) ) {
422  $specialMutePage = SpecialPage::getTitleFor( 'Mute', $sender->getName() );
423  $text .= "\n" . $context->msg(
424  'specialmute-email-footer',
425  $specialMutePage->getCanonicalURL(),
426  $sender->getName()
427  )->inContentLanguage()->text();
428  }
429 
430  // Check and increment the rate limits
431  if ( $sender->pingLimiter( 'emailuser' ) ) {
432  throw new ThrottledError();
433  }
434 
435  // Services that are needed, will be injected once this is moved to EmailUserUtils
436  // service, see T265541
437  $hookRunner = Hooks::runner();
438  $emailer = MediaWikiServices::getInstance()->getEmailer();
439 
440  $error = false;
441  if ( !$hookRunner->onEmailUser( $toAddress, $fromAddress, $subject, $text, $error ) ) {
442  if ( $error instanceof Status ) {
443  return $error;
444  } elseif ( $error === false || $error === '' || $error === [] ) {
445  // Possibly to tell HTMLForm to pretend there was no submission?
446  return false;
447  } elseif ( $error === true ) {
448  // Hook sent the mail itself and indicates success?
449  return Status::newGood();
450  } elseif ( is_array( $error ) ) {
451  $status = Status::newGood();
452  foreach ( $error as $e ) {
453  $status->fatal( $e );
454  }
455  return $status;
456  } elseif ( $error instanceof MessageSpecifier ) {
457  return Status::newFatal( $error );
458  } else {
459  // Setting $error to something else was deprecated in 1.29 and
460  // removed in 1.36, and so an exception is now thrown
461  $type = is_object( $error ) ? get_class( $error ) : gettype( $error );
462  throw new MWException(
463  'EmailUser hook set $error to unsupported type ' . $type
464  );
465  }
466  }
467 
468  if ( $config->get( MainConfigNames::UserEmailUseReplyTo ) ) {
477  $mailFrom = new MailAddress(
478  $config->get( MainConfigNames::PasswordSender ),
479  $context->msg( 'emailsender' )->inContentLanguage()->text()
480  );
481  $replyTo = $fromAddress;
482  } else {
498  $mailFrom = $fromAddress;
499  $replyTo = null;
500  }
501 
502  $status = Status::wrap( $emailer->send(
503  $toAddress,
504  $mailFrom,
505  $subject,
506  $text,
507  null,
508  [ 'replyTo' => $replyTo ]
509  ) );
510 
511  if ( !$status->isGood() ) {
512  return $status;
513  }
514 
515  // if the user requested a copy of this mail, do this now,
516  // unless they are emailing themselves, in which case one
517  // copy of the message is sufficient.
518  if ( $data['CCMe'] && $toAddress != $fromAddress ) {
519  $ccTo = $fromAddress;
520  $ccFrom = $fromAddress;
521  $ccSubject = $context->msg( 'emailccsubject' )->plaintextParams(
522  $target->getName(),
523  $subject
524  )->text();
525  $ccText = $text;
526 
527  $hookRunner->onEmailUserCC( $ccTo, $ccFrom, $ccSubject, $ccText );
528 
529  if ( $config->get( MainConfigNames::UserEmailUseReplyTo ) ) {
530  $mailFrom = new MailAddress(
531  $config->get( MainConfigNames::PasswordSender ),
532  $context->msg( 'emailsender' )->inContentLanguage()->text()
533  );
534  $replyTo = $ccFrom;
535  } else {
536  $mailFrom = $ccFrom;
537  $replyTo = null;
538  }
539 
540  $ccStatus = $emailer->send(
541  $ccTo,
542  $mailFrom,
543  $ccSubject,
544  $ccText,
545  null,
546  [ 'replyTo' => $replyTo ]
547  );
548  $status->merge( $ccStatus );
549  }
550 
551  $hookRunner->onEmailUserComplete( $toAddress, $fromAddress, $subject, $text );
552 
553  return $status;
554  }
555 
564  public function prefixSearchSubpages( $search, $limit, $offset ) {
565  $search = $this->userNameUtils->getCanonical( $search );
566  if ( !$search ) {
567  // No prefix suggestion for invalid user
568  return [];
569  }
570  // Autocomplete subpage as user list - public to allow caching
571  return $this->userNamePrefixSearch
572  ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
573  }
574 
575  protected function getGroupName() {
576  return 'users';
577  }
578 }
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
An error page which can definitely be safely rendered using the OutputPage.
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:349
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
MediaWiki exception.
Definition: MWException.php:29
Stores a single person's name and email address.
Definition: MailAddress.php:36
static newFromUser(UserEmailContact $user)
Create a new MailAddress object for the given user.
Definition: MailAddress.php:72
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Handles searching prefixes of user names.
UserNameUtils service.
Provides access to user options.
getOption(UserIdentity $user, string $oname, $defaultOverride=null, bool $ignoreHidden=false, int $queryFlags=self::READ_NORMAL)
Get the user's current setting for a given option.
Show an error when a user tries to do something they do not have the necessary permissions for.
A special page that allows users to send e-mails to other users.
static getTarget( $target, User $sender)
Validate target User.
userForm( $name)
Form to ask for target user name.
static getPermissionsError( $user, $editToken, Config $config=null)
Check whether a user is allowed to send email.
static validateTarget( $target, User $sender)
Validate target User.
static submit(array $data, IContextSource $context)
Really send a mail.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
User string $mTargetObj
doesWrites()
Indicates whether this special page may perform database writes.
__construct(UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, UserOptionsLookup $userOptionsLookup)
getDescription()
Returns the name that goes in the <h1> in the special page itself, and also the name that will be l...
execute( $par)
Default execute method Checks user permissions.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
isGood()
Returns whether the operation completed and didn't have any error or warnings.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:45
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:63
Show an error when the user hits a rate limit.
Shortcut to construct a special page which is unlisted by default.
Show an error when the user tries to do something whilst blocked.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
static newFromName( $name, $validate='valid')
Definition: User.php:587
isNewbie()
Determine whether the user is a newbie.
Definition: User.php:2918
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition: User.php:1634
Interface for configuration instances.
Definition: Config.php:30
Interface for objects which can provide a MediaWiki context on request.
getConfig()
Get the site configuration.
msg( $key,... $params)
This is the method for getting translated interface messages.