MediaWiki master
SpecialEmailUser.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
22use StatusValue;
23
33
34 private UserNameUtils $userNameUtils;
35 private UserNamePrefixSearch $userNamePrefixSearch;
36 private UserOptionsLookup $userOptionsLookup;
37 private EmailUserFactory $emailUserFactory;
38 private UserFactory $userFactory;
39
40 public function __construct(
41 UserNameUtils $userNameUtils,
42 UserNamePrefixSearch $userNamePrefixSearch,
43 UserOptionsLookup $userOptionsLookup,
44 EmailUserFactory $emailUserFactory,
45 UserFactory $userFactory
46 ) {
47 parent::__construct( 'Emailuser' );
48 $this->userNameUtils = $userNameUtils;
49 $this->userNamePrefixSearch = $userNamePrefixSearch;
50 $this->userOptionsLookup = $userOptionsLookup;
51 $this->emailUserFactory = $emailUserFactory;
52 $this->userFactory = $userFactory;
53 }
54
56 public function doesWrites() {
57 return true;
58 }
59
61 public function getDescription() {
62 return $this->msg( 'emailuser-title-notarget' );
63 }
64
65 protected function getFormFields( User $target ): array {
66 $linkRenderer = $this->getLinkRenderer();
67 $user = $this->getUser();
68 return [
69 'From' => [
70 'type' => 'info',
71 'raw' => 1,
72 'default' => $linkRenderer->makeLink(
73 $user->getUserPage(),
74 $user->getName()
75 ),
76 'label-message' => 'emailfrom',
77 'id' => 'mw-emailuser-sender',
78 ],
79 'To' => [
80 'type' => 'info',
81 'raw' => 1,
82 'default' => $linkRenderer->makeLink(
83 $target->getUserPage(),
84 $target->getName()
85 ),
86 'label-message' => 'emailto',
87 'id' => 'mw-emailuser-recipient',
88 ],
89 'Target' => [
90 'type' => 'hidden',
91 'default' => $target->getName(),
92 ],
93 'Subject' => [
94 'type' => 'text',
95 'default' => $this->msg( 'defemailsubject', $user->getName() )->inContentLanguage()->text(),
96 'label-message' => 'emailsubject',
97 'maxlength' => 200,
98 'size' => 60,
99 'required' => true,
100 ],
101 'Text' => [
102 'type' => 'textarea',
103 'rows' => 20,
104 'label-message' => 'emailmessage',
105 'required' => true,
106 ],
107 'CCMe' => [
108 'type' => 'check',
109 'label-message' => 'emailccme',
110 'default' => $this->userOptionsLookup->getBoolOption( $user, 'ccmeonemails' ),
111 ],
112 ];
113 }
114
118 private function handleCanSendEmailStatus( StatusValue $status ): void {
119 if ( !$status->isGood() ) {
120 if ( $status instanceof PermissionStatus ) {
121 $status->throwErrorPageError();
122 } elseif ( $status->hasMessage( 'mailnologin' ) ) {
123 throw new ErrorPageError( 'mailnologin', 'mailnologintext' );
124 } elseif ( $status->hasMessage( 'usermaildisabled' ) ) {
125 throw new ErrorPageError( 'usermaildisabled', 'usermaildisabledtext' );
126 } elseif ( $status->getValue() !== null ) {
127 // BC for deprecated hook errors
128 // (to be removed when UserCanSendEmail and EmailUserPermissionsErrors are removed)
129 $msg = $status->getMessages()[0];
130 throw new ErrorPageError( $status->getValue(), $msg );
131 } else {
132 // Fallback in case new error types are added in EmailUser
133 throw new ErrorPageError( $this->getDescription(), Status::wrap( $status )->getMessage() );
134 }
135 }
136 }
137
139 public function execute( $par ) {
140 $this->setHeaders();
141 $this->outputHeader();
142
143 $out = $this->getOutput();
144 $request = $this->getRequest();
145 $out->addModuleStyles( 'mediawiki.special' );
146
147 // Check if the user can send emails without authorizing the action.
148 $emailUser = $this->emailUserFactory->newEmailUserBC(
149 $this->getUser(),
150 $this->getConfig()
151 );
152 $emailUser->setEditToken( (string)$request->getVal( 'wpEditToken' ) );
153 $status = $emailUser->canSend();
154
155 // If user emailing is disabled, then prioritise this error over anything else (including the
156 // ::requireNamedUser check). This is because a user would still be unable access the form if they were to
157 // log in or create account.
158 if ( !$status->isGood() && $status->hasMessage( 'usermaildisabled' ) ) {
159 $this->handleCanSendEmailStatus( $status );
160 }
161
162 // If the user is not logged into a named account, then display this error. This should redirect to
163 // Special:UserLogin or Special:CreateAccount to not interrupt the flow.
164 $this->requireNamedUser( 'mailnologintext', 'mailnologin' );
165
166 $this->handleCanSendEmailStatus( $status );
167
168 // Always go through the userform, it will do validations on the target
169 // and display the emailform for us.
170 $target = $par ?? $request->getVal( 'wpTarget', $request->getVal( 'target', '' ) );
171 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Defaults to empty string
172 $this->userForm( $target );
173 }
174
183 public static function getTarget( $target, User $sender ) {
184 $targetObject = MediaWikiServices::getInstance()->getUserFactory()->newFromName( $target );
185 if ( !$targetObject instanceof User ) {
186 return 'notarget';
187 }
188
189 $status = MediaWikiServices::getInstance()->getEmailUserFactory()
190 ->newEmailUser( $sender )
191 ->validateTarget( $targetObject );
192 if ( !$status->isGood() ) {
193 $msg = $status->getMessages()[0]->getKey();
194 $ret = $msg === 'emailnotarget' ? 'notarget' : preg_replace( '/text$/', '', $msg );
195 } else {
196 $ret = $targetObject;
197 }
198 return $ret;
199 }
200
206 protected function userForm( $name ) {
207 $htmlForm = HTMLForm::factory( 'ooui', [
208 'Target' => [
209 'type' => 'user',
210 'exists' => true,
211 'required' => true,
212 'label-message' => 'emailusername',
213 'id' => 'emailusertarget',
214 'autofocus' => true,
215 // Exclude temporary accounts from the autocomplete, as they cannot have email addresses.
216 'excludetemp' => true,
217 // Skip validation when visit directly without subpage (T347854)
218 'default' => '',
219 // Prefill for subpage syntax and old target param.
220 'filter-callback' => static function ( $value ) use ( $name ) {
221 return str_replace( '_', ' ',
222 ( $value !== '' && $value !== false && $value !== null ) ? $value : $name );
223 },
224 'validation-callback' => function ( $value ) {
225 // HTMLForm checked that this is a valid user name
226 $target = $this->userFactory->newFromName( $value );
227 $statusValue = $this->emailUserFactory
228 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
229 ->newEmailUser( $this->getUser() )->validateTarget( $target );
230 if ( !$statusValue->isGood() ) {
231 // TODO: Return Status instead of StatusValue from validateTarget() method?
232 return Status::wrap( $statusValue )->getMessage();
233 }
234 return true;
235 }
236 ]
237 ], $this->getContext() );
238
239 $htmlForm
240 ->setMethod( 'GET' )
241 ->setTitle( $this->getPageTitle() ) // Remove subpage
242 ->setSubmitCallback( $this->sendEmailForm( ... ) )
243 ->setId( 'askusername' )
244 ->setWrapperLegendMsg( 'emailtarget' )
245 ->setSubmitTextMsg( 'emailusernamesubmit' )
246 ->show();
247 }
248
253 private function sendEmailForm( array $data ) {
254 $out = $this->getOutput();
255
256 // HTMLForm checked that this is a valid user name, the return value can never be null.
257 $target = $this->userFactory->newFromName( $data['Target'] );
258 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
259 $htmlForm = HTMLForm::factory( 'ooui', $this->getFormFields( $target ), $this->getContext() );
260 $htmlForm
261 ->setTitle( $this->getPageTitle() ) // Remove subpage
262 ->addPreHtml( $this->msg( 'emailpagetext', $target->getName() )->parse() )
263 ->setSubmitTextMsg( 'emailsend' )
264 ->setSubmitCallback( $this->onFormSubmit( ... ) )
265 ->setWrapperLegendMsg( 'email-legend' )
266 ->prepareForm();
267
268 if ( !$this->getHookRunner()->onEmailUserForm( $htmlForm ) ) {
269 return false;
270 }
271
272 $result = $htmlForm->show();
273
274 if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
275 $out->setPageTitleMsg( $this->msg( 'emailsent' ) );
276 $out->addWikiMsg( 'emailsenttext', $target->getName() );
277 $out->returnToMain( false, $target->getUserPage() );
278 } else {
279 $out->setPageTitleMsg( $this->msg( 'emailuser-title-target', $target->getName() ) );
280 }
281 return true;
282 }
283
288 private function onFormSubmit( array $data ) {
289 // HTMLForm checked that this is a valid user name, the return value can never be null.
290 $target = $this->userFactory->newFromName( $data['Target'] );
291
292 $emailUser = $this->emailUserFactory->newEmailUser( $this->getAuthority() );
293 $emailUser->setEditToken( $this->getRequest()->getVal( 'wpEditToken' ) );
294
295 // Fully authorize on sending emails.
296 $status = $emailUser->authorizeSend();
297
298 if ( !$status->isOK() ) {
299 return $status;
300 }
301
302 // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable
303 $res = $emailUser->sendEmailUnsafe(
304 $target,
305 $data['Subject'],
306 $data['Text'],
307 $data['CCMe'],
308 $this->getLanguage()->getCode()
309 );
310 if ( $res->hasMessage( 'hookaborted' ) ) {
311 // BC: The method could previously return false if the EmailUser hook set the error to false. Preserve
312 // that behaviour until we replace the hook.
313 $res = false;
314 } else {
315 $res = Status::wrap( $res );
316 }
317 return $res;
318 }
319
328 public function prefixSearchSubpages( $search, $limit, $offset ) {
329 $search = $this->userNameUtils->getCanonical( $search );
330 if ( !$search ) {
331 // No prefix suggestion for invalid user
332 return [];
333 }
334 // Autocomplete subpage as user list - public to allow caching
335 return $this->userNamePrefixSearch
336 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
337 }
338
342 public function isListed() {
343 return $this->getConfig()->get( MainConfigNames::EnableUserEmail );
344 }
345
347 protected function getGroupName() {
348 return 'users';
349 }
350}
351
353class_alias( SpecialEmailUser::class, 'SpecialEmailUser' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
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:195
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(UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, UserOptionsLookup $userOptionsLookup, EmailUserFactory $emailUserFactory, 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:1524
static newFromName( $name, $validate='valid')
Definition User.php:622
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.