MediaWiki REL1_37
SpecialEmailUser.php
Go to the documentation of this file.
1<?php
29
36 protected $mTarget;
37
41 protected $mTargetObj;
42
45
48
51
57 public function __construct(
61 ) {
62 parent::__construct( 'Emailuser' );
63 $this->userNameUtils = $userNameUtils;
64 $this->userNamePrefixSearch = $userNamePrefixSearch;
65 $this->userOptionsLookup = $userOptionsLookup;
66 }
67
68 public function doesWrites() {
69 return true;
70 }
71
72 public function getDescription() {
73 $target = self::getTarget( $this->mTarget, $this->getUser() );
74 if ( !$target instanceof User ) {
75 return $this->msg( 'emailuser-title-notarget' )->text();
76 }
77
78 return $this->msg( 'emailuser-title-target', $target->getName() )->text();
79 }
80
81 protected function getFormFields() {
83 $user = $this->getUser();
84 return [
85 'From' => [
86 'type' => 'info',
87 'raw' => 1,
88 'default' => $linkRenderer->makeLink(
89 $user->getUserPage(),
90 $user->getName()
91 ),
92 'label-message' => 'emailfrom',
93 'id' => 'mw-emailuser-sender',
94 ],
95 'To' => [
96 'type' => 'info',
97 'raw' => 1,
98 'default' => $linkRenderer->makeLink(
99 $this->mTargetObj->getUserPage(),
100 $this->mTargetObj->getName()
101 ),
102 'label-message' => 'emailto',
103 'id' => 'mw-emailuser-recipient',
104 ],
105 'Target' => [
106 'type' => 'hidden',
107 'default' => $this->mTargetObj->getName(),
108 ],
109 'Subject' => [
110 'type' => 'text',
111 'default' => $this->msg( 'defemailsubject', $user->getName() )->inContentLanguage()->text(),
112 'label-message' => 'emailsubject',
113 'maxlength' => 200,
114 'size' => 60,
115 'required' => true,
116 ],
117 'Text' => [
118 'type' => 'textarea',
119 'rows' => 20,
120 'label-message' => 'emailmessage',
121 'required' => true,
122 ],
123 'CCMe' => [
124 'type' => 'check',
125 'label-message' => 'emailccme',
126 'default' => $this->userOptionsLookup->getBoolOption( $user, 'ccmeonemails' ),
127 ],
128 ];
129 }
130
131 public function execute( $par ) {
132 $out = $this->getOutput();
133 $request = $this->getRequest();
134 $out->addModuleStyles( 'mediawiki.special' );
135
136 $this->mTarget = $par ?? $request->getVal( 'wpTarget', $request->getVal( 'target', '' ) );
137
138 // Make sure, that HTMLForm uses the correct target.
139 $request->setVal( 'wpTarget', $this->mTarget );
140
141 // This needs to be below assignment of $this->mTarget because
142 // getDescription() needs it to determine the correct page title.
143 $this->setHeaders();
144 $this->outputHeader();
145
146 // error out if sending user cannot do this
148 $this->getUser(),
149 $this->getRequest()->getVal( 'wpEditToken' ),
150 $this->getConfig()
151 );
152
153 switch ( $error ) {
154 case null:
155 # Wahey!
156 break;
157 case 'badaccess':
158 throw new PermissionsError( 'sendemail' );
159 case 'blockedemailuser':
160 throw new UserBlockedError( $this->getUser()->getBlock() );
161 case 'actionthrottledtext':
162 throw new ThrottledError;
163 case 'mailnologin':
164 case 'usermaildisabled':
165 throw new ErrorPageError( $error, "{$error}text" );
166 default:
167 # It's a hook error
168 list( $title, $msg, $params ) = $error;
169 throw new ErrorPageError( $title, $msg, $params );
170 }
171
172 // Make sure, that a submitted form isn't submitted to a subpage (which could be
173 // a non-existing username)
174 $context = new DerivativeContext( $this->getContext() );
175 $context->setTitle( $this->getPageTitle() ); // Remove subpage
176 $this->setContext( $context );
177
178 // A little hack: HTMLForm will check $this->mTarget only, if the form was posted, not
179 // if the user opens Special:EmailUser/Florian (e.g.). So check, if the user did that
180 // and show the "Send email to user" form directly, if so. Show the "enter username"
181 // form, otherwise.
182 $this->mTargetObj = self::getTarget( $this->mTarget, $this->getUser() );
183 if ( !$this->mTargetObj instanceof User ) {
184 $this->userForm( $this->mTarget );
185 } else {
186 $this->sendEmailForm();
187 }
188 }
189
197 public static function getTarget( $target, User $sender ) {
198 if ( $target == '' ) {
199 wfDebug( "Target is empty." );
200
201 return 'notarget';
202 }
203
204 $nu = User::newFromName( $target );
205 $error = self::validateTarget( $nu, $sender );
206
207 return $error ?: $nu;
208 }
209
218 public static function validateTarget( $target, User $sender ) {
219 if ( !$target instanceof User || !$target->getId() ) {
220 wfDebug( "Target is invalid user." );
221
222 return 'notarget';
223 }
224
225 if ( !$target->isEmailConfirmed() ) {
226 wfDebug( "User has no valid email." );
227
228 return 'noemail';
229 }
230
231 if ( !$target->canReceiveEmail() ) {
232 wfDebug( "User does not allow user emails." );
233
234 return 'nowikiemail';
235 }
236
237 if ( !$target->getOption( 'email-allow-new-users' ) && $sender->isNewbie() ) {
238 wfDebug( "User does not allow user emails from new users." );
239
240 return 'nowikiemail';
241 }
242
243 $muteList = $target->getOption( 'email-blacklist', '' );
244 if ( $muteList ) {
245 $muteList = MultiUsernameFilter::splitIds( $muteList );
246 $senderId = MediaWikiServices::getInstance()
247 ->getCentralIdLookup()
248 ->centralIdFromLocalUser( $sender );
249 if ( $senderId !== 0 && in_array( $senderId, $muteList ) ) {
250 wfDebug( "User does not allow user emails from this user." );
251
252 return 'nowikiemail';
253 }
254 }
255
256 return '';
257 }
258
268 public static function getPermissionsError( $user, $editToken, Config $config = null ) {
269 if ( $config === null ) {
270 wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
271 $config = MediaWikiServices::getInstance()->getMainConfig();
272 }
273 if ( !$config->get( 'EnableEmail' ) || !$config->get( 'EnableUserEmail' ) ) {
274 return 'usermaildisabled';
275 }
276
277 // Run this before checking 'sendemail' permission
278 // to show appropriate message to anons (T160309)
279 if ( !$user->isEmailConfirmed() ) {
280 return 'mailnologin';
281 }
282
283 if ( !MediaWikiServices::getInstance()
284 ->getPermissionManager()
285 ->userHasRight( $user, 'sendemail' )
286 ) {
287 return 'badaccess';
288 }
289
290 if ( $user->isBlockedFromEmailuser() ) {
291 wfDebug( "User is blocked from sending e-mail." );
292
293 return "blockedemailuser";
294 }
295
296 // Check the ping limiter without incrementing it - we'll check it
297 // again later and increment it on a successful send
298 if ( $user->pingLimiter( 'emailuser', 0 ) ) {
299 wfDebug( "Ping limiter triggered." );
300
301 return 'actionthrottledtext';
302 }
303
304 $hookErr = false;
305
306 Hooks::runner()->onUserCanSendEmail( $user, $hookErr );
307 Hooks::runner()->onEmailUserPermissionsErrors( $user, $editToken, $hookErr );
308
309 if ( $hookErr ) {
310 return $hookErr;
311 }
312
313 return null;
314 }
315
321 protected function userForm( $name ) {
322 $htmlForm = HTMLForm::factory( 'ooui', [
323 'Target' => [
324 'type' => 'user',
325 'exists' => true,
326 'required' => true,
327 'label' => $this->msg( 'emailusername' )->text(),
328 'id' => 'emailusertarget',
329 'autofocus' => true,
330 'value' => $name,
331 ]
332 ], $this->getContext() );
333
334 $htmlForm
335 ->setSubmitCallback( [ $this, 'sendEmailForm' ] )
336 ->setFormIdentifier( 'userForm' )
337 ->setId( 'askusername' )
338 ->setWrapperLegendMsg( 'emailtarget' )
339 ->setSubmitTextMsg( 'emailusernamesubmit' )
340 ->show();
341 }
342
343 public function sendEmailForm() {
344 $out = $this->getOutput();
345
346 $ret = $this->mTargetObj;
347 if ( !$ret instanceof User ) {
348 if ( $this->mTarget != '' ) {
349 // Messages used here: notargettext, noemailtext, nowikiemailtext
350 $ret = ( $ret == 'notarget' ) ? 'emailnotarget' : ( $ret . 'text' );
351 return Status::newFatal( $ret );
352 }
353 return false;
354 }
355
356 $htmlForm = HTMLForm::factory( 'ooui', $this->getFormFields(), $this->getContext() );
357 // By now we are supposed to be sure that $this->mTarget is a user name
358 $htmlForm
359 ->addPreText( $this->msg( 'emailpagetext', $this->mTarget )->parse() )
360 ->setSubmitTextMsg( 'emailsend' )
361 ->setSubmitCallback( [ __CLASS__, 'submit' ] )
362 ->setFormIdentifier( 'sendEmailForm' )
363 ->setWrapperLegendMsg( 'email-legend' )
364 ->loadData();
365
366 if ( !$this->getHookRunner()->onEmailUserForm( $htmlForm ) ) {
367 return false;
368 }
369
370 $result = $htmlForm->show();
371
372 if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
373 $out->setPageTitle( $this->msg( 'emailsent' ) );
374 $out->addWikiMsg( 'emailsenttext', $this->mTarget );
375 $out->returnToMain( false, $ret->getUserPage() );
376 }
377 return true;
378 }
379
390 public static function submit( array $data, IContextSource $context ) {
391 $config = $context->getConfig();
392
393 $sender = $context->getUser();
394 $target = self::getTarget( $data['Target'], $sender );
395 if ( !$target instanceof User ) {
396 // Messages used here: notargettext, noemailtext, nowikiemailtext
397 return Status::newFatal( $target . 'text' );
398 }
399
400 $toAddress = MailAddress::newFromUser( $target );
401 $fromAddress = MailAddress::newFromUser( $sender );
402 $subject = $data['Subject'];
403 $text = $data['Text'];
404
405 // Add a standard footer and trim up trailing newlines
406 $text = rtrim( $text ) . "\n\n-- \n";
407 $text .= $context->msg(
408 'emailuserfooter',
409 $fromAddress->name,
410 $toAddress->name
411 )->inContentLanguage()->text();
412
413 if ( $config->get( 'EnableSpecialMute' ) ) {
414 $specialMutePage = SpecialPage::getTitleFor( 'Mute', $sender->getName() );
415 $text .= "\n" . $context->msg(
416 'specialmute-email-footer',
417 $specialMutePage->getCanonicalURL(),
418 $sender->getName()
419 )->inContentLanguage()->text();
420 }
421
422 // Check and increment the rate limits
423 if ( $sender->pingLimiter( 'emailuser' ) ) {
424 throw new ThrottledError();
425 }
426
427 // Services that are needed, will be injected once this is moved to EmailUserUtils
428 // service, see T265541
429 $hookRunner = Hooks::runner();
430 $emailer = MediaWikiServices::getInstance()->getEmailer();
431
432 $error = false;
433 if ( !$hookRunner->onEmailUser( $toAddress, $fromAddress, $subject, $text, $error ) ) {
434 if ( $error instanceof Status ) {
435 return $error;
436 } elseif ( $error === false || $error === '' || $error === [] ) {
437 // Possibly to tell HTMLForm to pretend there was no submission?
438 return false;
439 } elseif ( $error === true ) {
440 // Hook sent the mail itself and indicates success?
441 return Status::newGood();
442 } elseif ( is_array( $error ) ) {
443 $status = Status::newGood();
444 foreach ( $error as $e ) {
445 $status->fatal( $e );
446 }
447 return $status;
448 } elseif ( $error instanceof MessageSpecifier ) {
449 return Status::newFatal( $error );
450 } else {
451 // Setting $error to something else was deprecated in 1.29 and
452 // removed in 1.36, and so an exception is now thrown
453 $type = is_object( $error ) ? get_class( $error ) : gettype( $error );
454 throw new MWException(
455 'EmailUser hook set $error to unsupported type ' . $type
456 );
457 }
458 }
459
460 if ( $config->get( 'UserEmailUseReplyTo' ) ) {
469 $mailFrom = new MailAddress(
470 $config->get( 'PasswordSender' ),
471 $context->msg( 'emailsender' )->inContentLanguage()->text()
472 );
473 $replyTo = $fromAddress;
474 } else {
490 $mailFrom = $fromAddress;
491 $replyTo = null;
492 }
493
494 $status = Status::wrap( $emailer->send(
495 $toAddress,
496 $mailFrom,
497 $subject,
498 $text,
499 null,
500 [ 'replyTo' => $replyTo ]
501 ) );
502
503 if ( !$status->isGood() ) {
504 return $status;
505 }
506
507 // if the user requested a copy of this mail, do this now,
508 // unless they are emailing themselves, in which case one
509 // copy of the message is sufficient.
510 if ( $data['CCMe'] && $toAddress != $fromAddress ) {
511 $ccTo = $fromAddress;
512 $ccFrom = $fromAddress;
513 $ccSubject = $context->msg( 'emailccsubject' )->plaintextParams(
514 $target->getName(),
515 $subject
516 )->text();
517 $ccText = $text;
518
519 $hookRunner->onEmailUserCC( $ccTo, $ccFrom, $ccSubject, $ccText );
520
521 if ( $config->get( 'UserEmailUseReplyTo' ) ) {
522 $mailFrom = new MailAddress(
523 $config->get( 'PasswordSender' ),
524 $context->msg( 'emailsender' )->inContentLanguage()->text()
525 );
526 $replyTo = $ccFrom;
527 } else {
528 $mailFrom = $ccFrom;
529 $replyTo = null;
530 }
531
532 $ccStatus = $emailer->send(
533 $ccTo,
534 $mailFrom,
535 $ccSubject,
536 $ccText,
537 null,
538 [ 'replyTo' => $replyTo ]
539 );
540 $status->merge( $ccStatus );
541 }
542
543 $hookRunner->onEmailUserComplete( $toAddress, $fromAddress, $subject, $text );
544
545 return $status;
546 }
547
556 public function prefixSearchSubpages( $search, $limit, $offset ) {
557 $search = $this->userNameUtils->getCanonical( $search );
558 if ( !$search ) {
559 // No prefix suggestion for invalid user
560 return [];
561 }
562 // Autocomplete subpage as user list - public to allow caching
563 return $this->userNamePrefixSearch
564 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
565 }
566
567 protected function getGroupName() {
568 return 'users';
569 }
570}
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
An IContextSource implementation which will inherit context from another source but allow individual ...
An error page which can definitely be safely rendered using the OutputPage.
MediaWiki exception.
Stores a single person's name and email address.
onEmailUserCC(&$to, &$from, &$subject, &$text)
This hook is called before sending the copy of the email to the author.
onEmailUser(&$to, &$from, &$subject, &$text, &$error)
This hook is called before sending email from one user to another.
onEmailUserComplete( $to, $from, $subject, $text)
This hook is called after sending email from one user to another.
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
MediaWikiServices is the service locator for the application scope of MediaWiki.
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...
setContext( $context)
Sets the context this SpecialPage is executed in.
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
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
HookRunner null $hookRunner
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.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
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:69
static newFromName( $name, $validate='valid')
Definition User.php:607
isNewbie()
Determine whether the user is a newbie.
Definition User.php:3674
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition User.php:2083
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.