MediaWiki REL1_39
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
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 $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.
MediaWiki exception.
Stores a single person's name and email address.
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.
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.
internal since 1.36
Definition User.php:70
static newFromName( $name, $validate='valid')
Definition User.php:598
isNewbie()
Determine whether the user is a newbie.
Definition User.php:2932
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition User.php:1646
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.