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 
49 
52 
58  public function __construct(
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  list( $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  if ( !$target->getOption( 'email-allow-new-users' ) && $sender->isNewbie() ) {
233  wfDebug( "User does not allow user emails from new users." );
234 
235  return 'nowikiemail';
236  }
237 
238  $muteList = $target->getOption( 'email-blacklist', '' );
239  if ( $muteList ) {
240  $muteList = MultiUsernameFilter::splitIds( $muteList );
241  $senderId = MediaWikiServices::getInstance()
242  ->getCentralIdLookup()
243  ->centralIdFromLocalUser( $sender );
244  if ( $senderId !== 0 && in_array( $senderId, $muteList ) ) {
245  wfDebug( "User does not allow user emails from this user." );
246 
247  return 'nowikiemail';
248  }
249  }
250 
251  return '';
252  }
253 
263  public static function getPermissionsError( $user, $editToken, Config $config = null ) {
264  if ( $config === null ) {
265  wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
266  $config = MediaWikiServices::getInstance()->getMainConfig();
267  }
268  if ( !$config->get( MainConfigNames::EnableEmail ) ||
269  !$config->get( MainConfigNames::EnableUserEmail ) ) {
270  return 'usermaildisabled';
271  }
272 
273  // Run this before checking 'sendemail' permission
274  // to show appropriate message to anons (T160309)
275  if ( !$user->isEmailConfirmed() ) {
276  return 'mailnologin';
277  }
278 
279  if ( !MediaWikiServices::getInstance()
280  ->getPermissionManager()
281  ->userHasRight( $user, 'sendemail' )
282  ) {
283  return 'badaccess';
284  }
285 
286  if ( $user->isBlockedFromEmailuser() ) {
287  wfDebug( "User is blocked from sending e-mail." );
288 
289  return "blockedemailuser";
290  }
291 
292  // Check the ping limiter without incrementing it - we'll check it
293  // again later and increment it on a successful send
294  if ( $user->pingLimiter( 'emailuser', 0 ) ) {
295  wfDebug( "Ping limiter triggered." );
296 
297  return 'actionthrottledtext';
298  }
299 
300  $hookErr = false;
301 
302  Hooks::runner()->onUserCanSendEmail( $user, $hookErr );
303  Hooks::runner()->onEmailUserPermissionsErrors( $user, $editToken, $hookErr );
304 
305  if ( $hookErr ) {
306  return $hookErr;
307  }
308 
309  return null;
310  }
311 
317  protected function userForm( $name ) {
318  $htmlForm = HTMLForm::factory( 'ooui', [
319  'Target' => [
320  'type' => 'user',
321  'exists' => true,
322  'required' => true,
323  'label' => $this->msg( 'emailusername' )->text(),
324  'id' => 'emailusertarget',
325  'autofocus' => true,
326  'value' => $name,
327  ]
328  ], $this->getContext() );
329 
330  $htmlForm
331  ->setTitle( $this->getPageTitle() ) // Remove subpage
332  ->setSubmitCallback( [ $this, 'sendEmailForm' ] )
333  ->setFormIdentifier( 'userForm' )
334  ->setId( 'askusername' )
335  ->setWrapperLegendMsg( 'emailtarget' )
336  ->setSubmitTextMsg( 'emailusernamesubmit' )
337  ->show();
338  }
339 
340  public function sendEmailForm() {
341  $out = $this->getOutput();
342 
343  $ret = $this->mTargetObj;
344  if ( !$ret instanceof User ) {
345  if ( $this->mTarget != '' ) {
346  // Messages used here: notargettext, noemailtext, nowikiemailtext
347  $ret = ( $ret == 'notarget' ) ? 'emailnotarget' : ( $ret . 'text' );
348  return Status::newFatal( $ret );
349  }
350  return false;
351  }
352 
353  $htmlForm = HTMLForm::factory( 'ooui', $this->getFormFields(), $this->getContext() );
354  // By now we are supposed to be sure that $this->mTarget is a user name
355  $htmlForm
356  ->setTitle( $this->getPageTitle() ) // Remove subpage
357  ->addPreText( $this->msg( 'emailpagetext', $this->mTarget )->parse() )
358  ->setSubmitTextMsg( 'emailsend' )
359  ->setSubmitCallback( [ __CLASS__, 'submit' ] )
360  ->setFormIdentifier( 'sendEmailForm' )
361  ->setWrapperLegendMsg( 'email-legend' )
362  ->prepareForm();
363 
364  if ( !$this->getHookRunner()->onEmailUserForm( $htmlForm ) ) {
365  return false;
366  }
367 
368  $result = $htmlForm->show();
369 
370  if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
371  $out->setPageTitle( $this->msg( 'emailsent' ) );
372  $out->addWikiMsg( 'emailsenttext', $this->mTarget );
373  $out->returnToMain( false, $ret->getUserPage() );
374  }
375  return true;
376  }
377 
388  public static function submit( array $data, IContextSource $context ) {
389  $config = $context->getConfig();
390 
391  $sender = $context->getUser();
392  $target = self::getTarget( $data['Target'], $sender );
393  if ( !$target instanceof User ) {
394  // Messages used here: notargettext, noemailtext, nowikiemailtext
395  return Status::newFatal( $target . 'text' );
396  }
397 
398  $toAddress = MailAddress::newFromUser( $target );
399  $fromAddress = MailAddress::newFromUser( $sender );
400  $subject = $data['Subject'];
401  $text = $data['Text'];
402 
403  // Add a standard footer and trim up trailing newlines
404  $text = rtrim( $text ) . "\n\n-- \n";
405  $text .= $context->msg(
406  'emailuserfooter',
407  $fromAddress->name,
408  $toAddress->name
409  )->inContentLanguage()->text();
410 
411  if ( $config->get( MainConfigNames::EnableSpecialMute ) ) {
412  $specialMutePage = SpecialPage::getTitleFor( 'Mute', $sender->getName() );
413  $text .= "\n" . $context->msg(
414  'specialmute-email-footer',
415  $specialMutePage->getCanonicalURL(),
416  $sender->getName()
417  )->inContentLanguage()->text();
418  }
419 
420  // Check and increment the rate limits
421  if ( $sender->pingLimiter( 'emailuser' ) ) {
422  throw new ThrottledError();
423  }
424 
425  // Services that are needed, will be injected once this is moved to EmailUserUtils
426  // service, see T265541
428  $emailer = MediaWikiServices::getInstance()->getEmailer();
429 
430  $error = false;
431  if ( !$hookRunner->onEmailUser( $toAddress, $fromAddress, $subject, $text, $error ) ) {
432  if ( $error instanceof Status ) {
433  return $error;
434  } elseif ( $error === false || $error === '' || $error === [] ) {
435  // Possibly to tell HTMLForm to pretend there was no submission?
436  return false;
437  } elseif ( $error === true ) {
438  // Hook sent the mail itself and indicates success?
439  return Status::newGood();
440  } elseif ( is_array( $error ) ) {
441  $status = Status::newGood();
442  foreach ( $error as $e ) {
443  $status->fatal( $e );
444  }
445  return $status;
446  } elseif ( $error instanceof MessageSpecifier ) {
447  return Status::newFatal( $error );
448  } else {
449  // Setting $error to something else was deprecated in 1.29 and
450  // removed in 1.36, and so an exception is now thrown
451  $type = is_object( $error ) ? get_class( $error ) : gettype( $error );
452  throw new MWException(
453  'EmailUser hook set $error to unsupported type ' . $type
454  );
455  }
456  }
457 
458  if ( $config->get( MainConfigNames::UserEmailUseReplyTo ) ) {
467  $mailFrom = new MailAddress(
468  $config->get( MainConfigNames::PasswordSender ),
469  $context->msg( 'emailsender' )->inContentLanguage()->text()
470  );
471  $replyTo = $fromAddress;
472  } else {
488  $mailFrom = $fromAddress;
489  $replyTo = null;
490  }
491 
492  $status = Status::wrap( $emailer->send(
493  $toAddress,
494  $mailFrom,
495  $subject,
496  $text,
497  null,
498  [ 'replyTo' => $replyTo ]
499  ) );
500 
501  if ( !$status->isGood() ) {
502  return $status;
503  }
504 
505  // if the user requested a copy of this mail, do this now,
506  // unless they are emailing themselves, in which case one
507  // copy of the message is sufficient.
508  if ( $data['CCMe'] && $toAddress != $fromAddress ) {
509  $ccTo = $fromAddress;
510  $ccFrom = $fromAddress;
511  $ccSubject = $context->msg( 'emailccsubject' )->plaintextParams(
512  $target->getName(),
513  $subject
514  )->text();
515  $ccText = $text;
516 
517  $hookRunner->onEmailUserCC( $ccTo, $ccFrom, $ccSubject, $ccText );
518 
519  if ( $config->get( MainConfigNames::UserEmailUseReplyTo ) ) {
520  $mailFrom = new MailAddress(
521  $config->get( MainConfigNames::PasswordSender ),
522  $context->msg( 'emailsender' )->inContentLanguage()->text()
523  );
524  $replyTo = $ccFrom;
525  } else {
526  $mailFrom = $ccFrom;
527  $replyTo = null;
528  }
529 
530  $ccStatus = $emailer->send(
531  $ccTo,
532  $mailFrom,
533  $ccSubject,
534  $ccText,
535  null,
536  [ 'replyTo' => $replyTo ]
537  );
538  $status->merge( $ccStatus );
539  }
540 
541  $hookRunner->onEmailUserComplete( $toAddress, $fromAddress, $subject, $text );
542 
543  return $status;
544  }
545 
554  public function prefixSearchSubpages( $search, $limit, $offset ) {
555  $search = $this->userNameUtils->getCanonical( $search );
556  if ( !$search ) {
557  // No prefix suggestion for invalid user
558  return [];
559  }
560  // Autocomplete subpage as user list - public to allow caching
561  return $this->userNamePrefixSearch
562  ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
563  }
564 
565  protected function getGroupName() {
566  return 'users';
567  }
568 }
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:344
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.
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.
UserNameUtils $userNameUtils
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)
UserOptionsLookup $userOptionsLookup
UserNamePrefixSearch $userNamePrefixSearch
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.
LinkRenderer null $linkRenderer
Definition: SpecialPage.php:81
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
HookRunner null $hookRunner
Definition: SpecialPage.php:86
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:70
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:82
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
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:68
static newFromName( $name, $validate='valid')
Definition: User.php:596
isNewbie()
Determine whether the user is a newbie.
Definition: User.php:3167
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition: User.php:1888
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.