MediaWiki  master
SpecialEmailUser.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Specials;
25 
26 use ErrorPageError;
27 use HTMLForm;
28 use IContextSource;
39 use Message;
41 use StatusValue;
42 use ThrottledError;
44 
51  protected $mTarget;
52 
56  protected $mTargetObj;
57 
58  private UserNameUtils $userNameUtils;
59  private UserNamePrefixSearch $userNamePrefixSearch;
60  private UserOptionsLookup $userOptionsLookup;
61  private EmailUserFactory $emailUserFactory;
62  private UserFactory $userFactory;
63 
71  public function __construct(
72  UserNameUtils $userNameUtils,
73  UserNamePrefixSearch $userNamePrefixSearch,
74  UserOptionsLookup $userOptionsLookup,
75  EmailUserFactory $emailUserFactory,
76  UserFactory $userFactory
77  ) {
78  parent::__construct( 'Emailuser' );
79  $this->userNameUtils = $userNameUtils;
80  $this->userNamePrefixSearch = $userNamePrefixSearch;
81  $this->userOptionsLookup = $userOptionsLookup;
82  $this->emailUserFactory = $emailUserFactory;
83  $this->userFactory = $userFactory;
84  }
85 
86  public function doesWrites() {
87  return true;
88  }
89 
90  public function getDescription() {
91  $target = self::getTarget( $this->mTarget, $this->getUser() );
92  if ( !$target instanceof User ) {
93  return $this->msg( 'emailuser-title-notarget' );
94  }
95 
96  return $this->msg( 'emailuser-title-target', $target->getName() );
97  }
98 
99  protected function getFormFields() {
100  $linkRenderer = $this->getLinkRenderer();
101  $user = $this->getUser();
102  return [
103  'From' => [
104  'type' => 'info',
105  'raw' => 1,
106  'default' => $linkRenderer->makeLink(
107  $user->getUserPage(),
108  $user->getName()
109  ),
110  'label-message' => 'emailfrom',
111  'id' => 'mw-emailuser-sender',
112  ],
113  'To' => [
114  'type' => 'info',
115  'raw' => 1,
116  'default' => $linkRenderer->makeLink(
117  $this->mTargetObj->getUserPage(),
118  $this->mTargetObj->getName()
119  ),
120  'label-message' => 'emailto',
121  'id' => 'mw-emailuser-recipient',
122  ],
123  'Target' => [
124  'type' => 'hidden',
125  'default' => $this->mTargetObj->getName(),
126  ],
127  'Subject' => [
128  'type' => 'text',
129  'default' => $this->msg( 'defemailsubject', $user->getName() )->inContentLanguage()->text(),
130  'label-message' => 'emailsubject',
131  'maxlength' => 200,
132  'size' => 60,
133  'required' => true,
134  ],
135  'Text' => [
136  'type' => 'textarea',
137  'rows' => 20,
138  'label-message' => 'emailmessage',
139  'required' => true,
140  ],
141  'CCMe' => [
142  'type' => 'check',
143  'label-message' => 'emailccme',
144  'default' => $this->userOptionsLookup->getBoolOption( $user, 'ccmeonemails' ),
145  ],
146  ];
147  }
148 
149  public function execute( $par ) {
150  $out = $this->getOutput();
151  $request = $this->getRequest();
152  $out->addModuleStyles( 'mediawiki.special' );
153 
154  $this->mTarget = $par ?? $request->getVal( 'wpTarget', $request->getVal( 'target', '' ) );
155 
156  // This needs to be below assignment of $this->mTarget because
157  // getDescription() needs it to determine the correct page title.
158  $this->setHeaders();
159  $this->outputHeader();
160 
161  // Error out if sending user cannot do this. Don't authorize yet.
162  $error = self::getPermissionsError(
163  $this->getUser(),
164  $this->getRequest()->getVal( 'wpEditToken' ),
165  $this->getConfig()
166  );
167 
168  switch ( $error ) {
169  case null:
170  # Wahey!
171  break;
172  case 'badaccess':
173  throw new PermissionsError( 'sendemail' );
174  case 'blockedemailuser':
175  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
176  throw new UserBlockedError( $this->getUser()->getBlock() );
177  case 'actionthrottledtext':
178  throw new ThrottledError;
179  case 'mailnologin':
180  case 'usermaildisabled':
181  throw new ErrorPageError( $error, "{$error}text" );
182  default:
183  # It's a hook error
184  [ $title, $msg, $params ] = $error;
185  throw new ErrorPageError( $title, $msg, $params );
186  }
187 
188  // A little hack: HTMLForm will check wpTarget only, if the form was posted, not
189  // if the user opens Special:EmailUser/Florian (e.g.). So check, if the user did that
190  // and show the "Send email to user" form directly, if so. Show the "enter username"
191  // form, otherwise.
192  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable target is set
193  $this->mTargetObj = self::getTarget( $this->mTarget, $this->getUser() );
194  if ( !$this->mTargetObj instanceof User ) {
195  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable target is set
196  $this->userForm( $this->mTarget );
197  } else {
198  $this->sendEmailForm();
199  }
200  }
201 
209  public static function getTarget( $target, User $sender ) {
210  $targetObject = MediaWikiServices::getInstance()->getUserFactory()->newFromName( $target );
211  if ( !$targetObject instanceof User ) {
212  return 'notarget';
213  }
214 
215  $status = MediaWikiServices::getInstance()->getEmailUserFactory()
216  ->newEmailUser( $sender )
217  ->validateTarget( $targetObject );
218  if ( !$status->isGood() ) {
219  $msg = $status->getErrors()[0]['message'];
220  $ret = $msg === 'emailnotarget' ? 'notarget' : preg_replace( '/text$/', '', $msg );
221  } else {
222  $ret = $targetObject;
223  }
224  return $ret;
225  }
226 
235  public static function validateTarget( $target, User $sender ) {
236  if ( !$target instanceof User ) {
237  return 'notarget';
238  }
239  $status = MediaWikiServices::getInstance()->getEmailUserFactory()
240  ->newEmailUser( $sender )
241  ->validateTarget( $target );
242  if ( $status->isGood() ) {
243  $ret = '';
244  } else {
245  $msg = $status->getErrors()[0]['message'];
246  $ret = $msg === 'emailnotarget' ? 'notarget' : preg_replace( '/text$/', '', $msg );
247  }
248  return $ret;
249  }
250 
263  public static function getPermissionsError( $user, $editToken, Config $config = null, $authorize = false ) {
264  $emailUser = MediaWikiServices::getInstance()->getEmailUserFactory()->newEmailUserBC( $user, $config );
265  $status = $authorize ? $emailUser->authorizeSend( (string)$editToken )
266  : $emailUser->canSend( (string)$editToken );
267 
268  if ( $status->isGood() ) {
269  return null;
270  }
271  foreach ( $status->getErrors() as $err ) {
272  $errKey = $err['message'] instanceof Message ? $err['message']->getKey() : $err['message'];
273  if ( strpos( $errKey, 'blockedtext' ) !== false ) {
274  // BC for block messages
275  return "blockedemailuser";
276  }
277  }
278  $error = $status->getErrors()[0];
279  if ( $status->getValue() !== null ) {
280  // BC for hook errors intended to be used with ErrorPageError
281  return [ $status->getValue(), $error['message'], $error['params'] ];
282  }
283  return $error['message'];
284  }
285 
291  protected function userForm( $name ) {
292  $htmlForm = HTMLForm::factory( 'ooui', [
293  'Target' => [
294  'type' => 'user',
295  'exists' => true,
296  'required' => true,
297  'label-message' => 'emailusername',
298  'id' => 'emailusertarget',
299  'autofocus' => true,
300  // Prefill for subpage syntax and old target param.
301  'filter-callback' => static function ( $value ) use ( $name ) {
302  return $value ?? $name;
303  }
304  ]
305  ], $this->getContext() );
306 
307  $htmlForm
308  ->setMethod( 'GET' )
309  ->setTitle( $this->getPageTitle() ) // Remove subpage
310  ->setSubmitCallback( [ $this, 'sendEmailForm' ] )
311  ->setId( 'askusername' )
312  ->setWrapperLegendMsg( 'emailtarget' )
313  ->setSubmitTextMsg( 'emailusernamesubmit' )
314  ->show();
315  }
316 
317  public function sendEmailForm() {
318  $out = $this->getOutput();
319 
320  if ( !$this->mTargetObj instanceof User ) {
321  if ( $this->mTarget != '' ) {
322  // Messages used here: noemailtext, nowikiemailtext
323  $msg = ( $this->mTargetObj === 'notarget' ) ? 'emailnotarget' : ( $this->mTargetObj . 'text' );
324  return Status::newFatal( $msg );
325  }
326  return false;
327  }
328 
329  $htmlForm = HTMLForm::factory( 'ooui', $this->getFormFields(), $this->getContext() );
330  // By now we are supposed to be sure that $this->mTarget is a user name
331  $htmlForm
332  ->setTitle( $this->getPageTitle() ) // Remove subpage
333  ->addPreHtml( $this->msg( 'emailpagetext', $this->mTarget )->parse() )
334  ->setSubmitTextMsg( 'emailsend' )
335  ->setSubmitCallback( [ $this, 'onFormSubmit' ] )
336  ->setWrapperLegendMsg( 'email-legend' )
337  ->prepareForm();
338 
339  if ( !$this->getHookRunner()->onEmailUserForm( $htmlForm ) ) {
340  return false;
341  }
342 
343  $result = $htmlForm->show();
344 
345  if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
346  $out->setPageTitleMsg( $this->msg( 'emailsent' ) );
347  $out->addWikiMsg( 'emailsenttext', $this->mTarget );
348  $out->returnToMain( false, $this->mTargetObj->getUserPage() );
349  }
350  return true;
351  }
352 
357  public function onFormSubmit( array $data ) {
358  $target = $this->userFactory->newFromName( $data['Target'] );
359  if ( !$target instanceof User ) {
360  return StatusValue::newFatal( 'emailnotarget' );
361  }
362 
363  $emailUser = $this->emailUserFactory->newEmailUser( $this->getAuthority() );
364 
365  // Fully authorize on sending emails.
366  $status = $emailUser->authorizeSend( $this->getRequest()->getVal( 'wpEditToken' ) );
367 
368  if ( !$status->isOK() ) {
369  return $status;
370  }
371 
372  $res = $emailUser->sendEmailUnsafe(
373  $target,
374  $data['Subject'],
375  $data['Text'],
376  $data['CCMe'],
377  $this->getLanguage()->getCode()
378  );
379  if ( $res->hasMessage( 'hookaborted' ) ) {
380  // BC: The method could previously return false if the EmailUser hook set the error to false. Preserve
381  // that behaviour until we replace the hook.
382  $res = false;
383  } else {
384  $res = Status::wrap( $res );
385  }
386  return $res;
387  }
388 
400  public static function submit( array $data, IContextSource $context ) {
401  $target = MediaWikiServices::getInstance()->getUserFactory()->newFromName( (string)$data['Target'] );
402  if ( !$target instanceof User ) {
403  return Status::newFatal( 'emailnotarget' );
404  }
405 
406  $emailUser = MediaWikiServices::getInstance()->getEmailUserFactory()
407  ->newEmailUserBC( $context->getAuthority(), $context->getConfig() );
408 
409  $ret = $emailUser->sendEmailUnsafe(
410  $target,
411  (string)$data['Subject'],
412  (string)$data['Text'],
413  (bool)$data['CCMe'],
414  $context->getLanguage()->getCode()
415  );
416  if ( $ret->hasMessage( 'hookaborted' ) ) {
417  // BC: The method could previously return false if the EmailUser hook set the error to false.
418  $ret = false;
419  } elseif ( $ret->hasMessage( 'noemailtarget' ) ) {
420  // BC: The previous implementation would use notargettext even if noemailtarget would be the right
421  // message to use here.
422  return Status::newFatal( 'notargettext' );
423  } else {
424  $ret = Status::wrap( $ret );
425  }
426  return $ret;
427  }
428 
437  public function prefixSearchSubpages( $search, $limit, $offset ) {
438  $search = $this->userNameUtils->getCanonical( $search );
439  if ( !$search ) {
440  // No prefix suggestion for invalid user
441  return [];
442  }
443  // Autocomplete subpage as user list - public to allow caching
444  return $this->userNamePrefixSearch
445  ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
446  }
447 
448  protected function getGroupName() {
449  return 'users';
450  }
451 }
452 
456 class_alias( SpecialEmailUser::class, 'SpecialEmailUser' );
An error page which can definitely be safely rendered using the OutputPage.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition: HTMLForm.php:158
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:360
Factory for EmailUser objects.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getUser()
Shortcut to get the User executing this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
Shortcut to construct a special page which is unlisted by default.
A special page that allows users to send e-mails to other users.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
getDescription()
Returns the name that goes in the <h1> in the special page itself, and also the name that will be l...
static validateTarget( $target, User $sender)
Validate target User.
userForm( $name)
Form to ask for target user name.
doesWrites()
Indicates whether this special page may perform database writes.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
static getTarget( $target, User $sender)
Validate target User.
execute( $par)
Default execute method Checks user permissions.
static getPermissionsError( $user, $editToken, Config $config=null, $authorize=false)
Check whether a user is allowed to send email.
__construct(UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, UserOptionsLookup $userOptionsLookup, EmailUserFactory $emailUserFactory, UserFactory $userFactory)
static submit(array $data, IContextSource $context)
Really send a mail.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:76
Creates User objects.
Definition: UserFactory.php:41
Handles searching prefixes of user names.
UserNameUtils service.
Provides access to user options.
internal since 1.36
Definition: User.php:98
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:144
getKey()
Returns the message key.
Definition: Message.php:365
Show an error when a user tries to do something they do not have the necessary permissions for.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:46
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.
Show an error when the user hits a rate limit.
Show an error when the user tries to do something whilst blocked.
Interface for objects which can provide a MediaWiki context on request.
getConfig()
Get the site configuration.
Interface for configuration instances.
Definition: Config.php:32