MediaWiki master
SpecialEmailUser.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
22use StatusValue;
23
33
34 public function __construct(
35 private readonly UserNameUtils $userNameUtils,
36 private readonly UserNamePrefixSearch $userNamePrefixSearch,
37 private readonly UserOptionsLookup $userOptionsLookup,
38 private readonly EmailUserFactory $emailUserFactory,
39 private readonly UserFactory $userFactory,
40 ) {
41 parent::__construct( 'Emailuser' );
42 }
43
45 public function doesWrites() {
46 return true;
47 }
48
50 public function getDescription() {
51 return $this->msg( 'emailuser-title-notarget' );
52 }
53
54 protected function getFormFields( User $target ): array {
55 $linkRenderer = $this->getLinkRenderer();
56 $user = $this->getUser();
57 return [
58 'From' => [
59 'type' => 'info',
60 'raw' => 1,
61 'default' => $linkRenderer->makeLink(
62 $user->getUserPage(),
63 $user->getName()
64 ),
65 'label-message' => 'emailfrom',
66 'id' => 'mw-emailuser-sender',
67 ],
68 'To' => [
69 'type' => 'info',
70 'raw' => 1,
71 'default' => $linkRenderer->makeLink(
72 $target->getUserPage(),
73 $target->getName()
74 ),
75 'label-message' => 'emailto',
76 'id' => 'mw-emailuser-recipient',
77 ],
78 'Target' => [
79 'type' => 'hidden',
80 'default' => $target->getName(),
81 ],
82 'Subject' => [
83 'type' => 'text',
84 'default' => $this->msg( 'defemailsubject', $user->getName() )->inContentLanguage()->text(),
85 'label-message' => 'emailsubject',
86 'maxlength' => 200,
87 'size' => 60,
88 'required' => true,
89 ],
90 'Text' => [
91 'type' => 'textarea',
92 'rows' => 20,
93 'label-message' => 'emailmessage',
94 'required' => true,
95 ],
96 'CCMe' => [
97 'type' => 'check',
98 'label-message' => 'emailccme',
99 'default' => $this->userOptionsLookup->getBoolOption( $user, 'ccmeonemails' ),
100 ],
101 ];
102 }
103
107 private function handleCanSendEmailStatus( StatusValue $status ): void {
108 if ( !$status->isGood() ) {
109 if ( $status instanceof PermissionStatus ) {
110 $status->throwErrorPageError();
111 } elseif ( $status->hasMessage( 'mailnologin' ) ) {
112 throw new ErrorPageError( 'mailnologin', 'mailnologintext' );
113 } elseif ( $status->hasMessage( 'usermaildisabled' ) ) {
114 throw new ErrorPageError( 'usermaildisabled', 'usermaildisabledtext' );
115 } elseif ( $status->getValue() !== null ) {
116 // BC for deprecated hook errors
117 // (to be removed when UserCanSendEmail and EmailUserPermissionsErrors are removed)
118 $msg = $status->getMessages()[0];
119 throw new ErrorPageError( $status->getValue(), $msg );
120 } else {
121 // Fallback in case new error types are added in EmailUser
122 throw new ErrorPageError( $this->getDescription(), Status::wrap( $status )->getMessage() );
123 }
124 }
125 }
126
128 public function execute( $par ) {
129 $this->setHeaders();
130 $this->outputHeader();
131
132 $out = $this->getOutput();
133 $request = $this->getRequest();
134 $out->addModuleStyles( 'mediawiki.special' );
135
136 // Check if the user can send emails without authorizing the action.
137 $emailUser = $this->emailUserFactory->newEmailUserBC(
138 $this->getUser(),
139 $this->getConfig()
140 );
141 $emailUser->setEditToken( (string)$request->getVal( 'wpEditToken' ) );
142 $status = $emailUser->canSend();
143
144 // If user emailing is disabled, then prioritise this error over anything else (including the
145 // ::requireNamedUser check). This is because a user would still be unable access the form if they were to
146 // log in or create account.
147 if ( !$status->isGood() && $status->hasMessage( 'usermaildisabled' ) ) {
148 $this->handleCanSendEmailStatus( $status );
149 }
150
151 // If the user is not logged into a named account, then display this error. This should redirect to
152 // Special:UserLogin or Special:CreateAccount to not interrupt the flow.
153 $this->requireNamedUser( 'mailnologintext', 'mailnologin' );
154
155 $this->handleCanSendEmailStatus( $status );
156
157 // Always go through the userform, it will do validations on the target
158 // and display the emailform for us.
159 $target = $par ?? $request->getVal( 'wpTarget', $request->getVal( 'target', '' ) );
160 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Defaults to empty string
161 $this->userForm( $target );
162 }
163
172 public static function getTarget( $target, User $sender ) {
173 $targetObject = MediaWikiServices::getInstance()->getUserFactory()->newFromName( $target );
174 if ( !$targetObject instanceof User ) {
175 return 'notarget';
176 }
177
178 $status = MediaWikiServices::getInstance()->getEmailUserFactory()
179 ->newEmailUser( $sender )
180 ->validateTarget( $targetObject );
181 if ( !$status->isGood() ) {
182 $msg = $status->getMessages()[0]->getKey();
183 $ret = $msg === 'emailnotarget' ? 'notarget' : preg_replace( '/text$/', '', $msg );
184 } else {
185 $ret = $targetObject;
186 }
187 return $ret;
188 }
189
195 protected function userForm( $name ) {
196 $htmlForm = HTMLForm::factory( 'ooui', [
197 'Target' => [
198 'type' => 'user',
199 'exists' => true,
200 'required' => true,
201 'label-message' => 'emailusername',
202 'id' => 'emailusertarget',
203 'autofocus' => true,
204 // Exclude temporary accounts from the autocomplete, as they cannot have email addresses.
205 'excludetemp' => true,
206 // Skip validation when visit directly without subpage (T347854)
207 'default' => '',
208 // Prefill for subpage syntax and old target param.
209 'filter-callback' => static function ( $value ) use ( $name ) {
210 return str_replace( '_', ' ',
211 ( $value !== '' && $value !== false && $value !== null ) ? $value : $name );
212 },
213 'validation-callback' => function ( $value ) {
214 // HTMLForm checked that this is a valid user name
215 $target = $this->userFactory->newFromName( $value );
216 $statusValue = $this->emailUserFactory
217 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
218 ->newEmailUser( $this->getUser() )->validateTarget( $target );
219 if ( !$statusValue->isGood() ) {
220 // TODO: Return Status instead of StatusValue from validateTarget() method?
221 return Status::wrap( $statusValue )->getMessage();
222 }
223 return true;
224 }
225 ]
226 ], $this->getContext() );
227
228 $htmlForm
229 ->setMethod( 'GET' )
230 ->setTitle( $this->getPageTitle() ) // Remove subpage
231 ->setSubmitCallback( $this->sendEmailForm( ... ) )
232 ->setId( 'askusername' )
233 ->setWrapperLegendMsg( 'emailtarget' )
234 ->setSubmitTextMsg( 'emailusernamesubmit' )
235 ->show();
236 }
237
242 private function sendEmailForm( array $data ) {
243 $out = $this->getOutput();
244
245 // HTMLForm checked that this is a valid user name, the return value can never be null.
246 $target = $this->userFactory->newFromName( $data['Target'] );
247 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
248 $htmlForm = HTMLForm::factory( 'ooui', $this->getFormFields( $target ), $this->getContext() );
249 $htmlForm
250 ->setTitle( $this->getPageTitle() ) // Remove subpage
251 ->addPreHtml( $this->msg( 'emailpagetext', $target->getName() )->parse() )
252 ->setSubmitTextMsg( 'emailsend' )
253 ->setSubmitCallback( $this->onFormSubmit( ... ) )
254 ->setWrapperLegendMsg( 'email-legend' )
255 ->prepareForm();
256
257 if ( !$this->getHookRunner()->onEmailUserForm( $htmlForm ) ) {
258 return false;
259 }
260
261 $result = $htmlForm->show();
262
263 if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
264 $out->setPageTitleMsg( $this->msg( 'emailsent' ) );
265 $out->addWikiMsg( 'emailsenttext', $target->getName() );
266 $out->returnToMain( false, $target->getUserPage() );
267 } else {
268 $out->setPageTitleMsg( $this->msg( 'emailuser-title-target', $target->getName() ) );
269 }
270 return true;
271 }
272
277 private function onFormSubmit( array $data ) {
278 // HTMLForm checked that this is a valid user name, the return value can never be null.
279 $target = $this->userFactory->newFromName( $data['Target'] );
280
281 $emailUser = $this->emailUserFactory->newEmailUser( $this->getAuthority() );
282 $emailUser->setEditToken( $this->getRequest()->getVal( 'wpEditToken' ) );
283
284 // Fully authorize on sending emails.
285 $status = $emailUser->authorizeSend();
286
287 if ( !$status->isOK() ) {
288 return $status;
289 }
290
291 // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable
292 $res = $emailUser->sendEmailUnsafe(
293 $target,
294 $data['Subject'],
295 $data['Text'],
296 $data['CCMe'],
297 $this->getLanguage()->getCode()
298 );
299 if ( $res->hasMessage( 'hookaborted' ) ) {
300 // BC: The method could previously return false if the EmailUser hook set the error to false. Preserve
301 // that behaviour until we replace the hook.
302 $res = false;
303 } else {
304 $res = Status::wrap( $res );
305 }
306 return $res;
307 }
308
317 public function prefixSearchSubpages( $search, $limit, $offset ) {
318 $search = $this->userNameUtils->getCanonical( $search );
319 if ( !$search ) {
320 // No prefix suggestion for invalid user
321 return [];
322 }
323 // Autocomplete subpage as user list - public to allow caching
324 return $this->userNamePrefixSearch
325 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
326 }
327
331 public function isListed() {
332 return $this->getConfig()->get( MainConfigNames::EnableUserEmail );
333 }
334
336 protected function getGroupName() {
337 return 'users';
338 }
339}
340
342class_alias( SpecialEmailUser::class, 'SpecialEmailUser' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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:207
Factory for EmailUser objects.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
A StatusValue for permission errors.
Parent class for all special pages.
getUser()
Shortcut to get the User executing this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Send an e-mail from one user to another.
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...
userForm( $name)
Form to ask for target user name.
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki....
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.This must be overridden by subclasses; it will be made...
__construct(private readonly UserNameUtils $userNameUtils, private readonly UserNamePrefixSearch $userNamePrefixSearch, private readonly UserOptionsLookup $userOptionsLookup, private readonly EmailUserFactory $emailUserFactory, private readonly UserFactory $userFactory,)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Provides access to user options.
Create User objects.
Handles searching prefixes of user names.
UserNameUtils service.
User class for the MediaWiki software.
Definition User.php:130
getUserPage()
Get this user's personal page title.
Definition User.php:2710
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1525
static newFromName( $name, $validate='valid')
Definition User.php:624
Generic operation result class Has warning/error list, boolean status and arbitrary value.
getMessages(?string $type=null)
Returns a list of error messages, optionally only those of the given type.
hasMessage(string $message)
Returns true if the specified message is present as a warning or error.
isOK()
Returns whether the operation completed.
isGood()
Returns whether the operation completed and didn't have any error or warnings.