Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
49.44% |
176 / 356 |
|
33.33% |
4 / 12 |
CRAP | |
0.00% |
0 / 1 |
SpecialContact | |
49.44% |
176 / 356 |
|
33.33% |
4 / 12 |
1474.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTypeConfig | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getFormSpecificMessageKey | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
execute | |
80.39% |
41 / 51 |
|
0.00% |
0 / 1 |
14.27 | |||
checkFormErrors | |
92.59% |
25 / 27 |
|
0.00% |
0 / 1 |
17.12 | |||
getFormFields | |
92.63% |
88 / 95 |
|
0.00% |
0 / 1 |
14.08 | |||
processInput | |
0.00% |
0 / 142 |
|
0.00% |
0 / 1 |
1640 | |||
getYesOrNoMsg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
useCaptcha | |
33.33% |
3 / 9 |
|
0.00% |
0 / 1 |
12.41 | |||
getCaptcha | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getContactPageHookRunner | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Speclial:Contact, a contact form for visitors. |
4 | * Based on SpecialEmailUser.php |
5 | * |
6 | * @file |
7 | * @ingroup SpecialPage |
8 | * @author Daniel Kinzler, brightbyte.de |
9 | * @copyright © 2007-2014 Daniel Kinzler, Sam Reed |
10 | * @license GPL-2.0-or-later |
11 | */ |
12 | |
13 | namespace MediaWiki\Extension\ContactPage; |
14 | |
15 | use ErrorPageError; |
16 | use MailAddress; |
17 | use MediaWiki\Extension\ConfirmEdit\Hooks as ConfirmEditHooks; |
18 | use MediaWiki\Extension\ContactPage\Hooks\HookRunner; |
19 | use MediaWiki\Html\Html; |
20 | use MediaWiki\HTMLForm\Field\HTMLCheckField; |
21 | use MediaWiki\HTMLForm\Field\HTMLHiddenField; |
22 | use MediaWiki\HTMLForm\HTMLForm; |
23 | use MediaWiki\MainConfigNames; |
24 | use MediaWiki\Parser\Sanitizer; |
25 | use MediaWiki\Registration\ExtensionRegistry; |
26 | use MediaWiki\Session\SessionManager; |
27 | use MediaWiki\SpecialPage\UnlistedSpecialPage; |
28 | use MediaWiki\Status\Status; |
29 | use MediaWiki\User\Options\UserOptionsLookup; |
30 | use MediaWiki\User\User; |
31 | use MediaWiki\User\UserFactory; |
32 | use UserBlockedError; |
33 | use UserMailer; |
34 | |
35 | /** |
36 | * Provides the contact form |
37 | * @ingroup SpecialPage |
38 | */ |
39 | class SpecialContact extends UnlistedSpecialPage { |
40 | private UserOptionsLookup $userOptionsLookup; |
41 | private UserFactory $userFactory; |
42 | /** @var HookRunner|null */ |
43 | private $contactPageHookRunner; |
44 | |
45 | /** @var string|null */ |
46 | private $recipientName = null; |
47 | |
48 | /** |
49 | * @param UserOptionsLookup $userOptionsLookup |
50 | * @param UserFactory $userFactory |
51 | */ |
52 | public function __construct( UserOptionsLookup $userOptionsLookup, UserFactory $userFactory ) { |
53 | parent::__construct( 'Contact' ); |
54 | $this->userOptionsLookup = $userOptionsLookup; |
55 | $this->userFactory = $userFactory; |
56 | } |
57 | |
58 | /** |
59 | * @inheritDoc |
60 | */ |
61 | public function getDescription() { |
62 | return $this->msg( 'contactpage' ); |
63 | } |
64 | |
65 | /** |
66 | * @var string |
67 | */ |
68 | protected $formType; |
69 | |
70 | /** |
71 | * @return array |
72 | */ |
73 | protected function getTypeConfig() { |
74 | $contactConfig = $this->getConfig()->get( 'ContactConfig' ); |
75 | |
76 | if ( $contactConfig['default']['SenderName'] === null ) { |
77 | $sitename = $this->getConfig()->get( 'Sitename' ); |
78 | $contactConfig['default']['SenderName'] = "Contact Form on $sitename"; |
79 | } |
80 | |
81 | if ( isset( $contactConfig[$this->formType] ) ) { |
82 | return $contactConfig[$this->formType] + $contactConfig['default']; |
83 | } |
84 | return $contactConfig['default']; |
85 | } |
86 | |
87 | /** |
88 | * Helper function that returns a form-specific message key if it is not |
89 | * disabled. Otherwise returns the generic message key. Used to make it |
90 | * possible for forms to have form-specific messages. |
91 | * |
92 | * @param string $genericMessageKey The message key that will be used if no form-specific one can be used |
93 | * @return string |
94 | */ |
95 | protected function getFormSpecificMessageKey( string $genericMessageKey ): string { |
96 | $formSpecificMessageKey = $genericMessageKey . '-' . $this->formType; |
97 | if ( !str_starts_with( $genericMessageKey, 'contactpage' ) ) { |
98 | // If the generic message does not start with "contactpage" the form |
99 | // specific one will have "contactpage-" prefixed on the generic message |
100 | // name. |
101 | $formSpecificMessageKey = 'contactpage-' . $formSpecificMessageKey; |
102 | } |
103 | if ( $this->formType && !$this->msg( $formSpecificMessageKey )->isDisabled() ) { |
104 | // Return the form-specific message if the form type is not the empty string |
105 | // and the message is defined. |
106 | return $formSpecificMessageKey; |
107 | } |
108 | return $genericMessageKey; |
109 | } |
110 | |
111 | /** |
112 | * Main execution function |
113 | * |
114 | * @param string|null $par Parameters passed to the page |
115 | * @throws UserBlockedError |
116 | * @throws ErrorPageError |
117 | */ |
118 | public function execute( $par ) { |
119 | if ( !$this->getConfig()->get( MainConfigNames::EnableEmail ) ) { |
120 | // From Special:EmailUser |
121 | throw new ErrorPageError( 'usermaildisabled', 'usermaildisabledtext' ); |
122 | } |
123 | |
124 | $request = $this->getRequest(); |
125 | $this->formType = strtolower( $request->getText( 'formtype', $par ?? '' ) ); |
126 | |
127 | $config = $this->getTypeConfig(); |
128 | |
129 | if ( $config['Redirect'] ) { |
130 | $this->getOutput()->redirect( $config['Redirect'] ); |
131 | return; |
132 | } |
133 | |
134 | $user = $this->getUser(); |
135 | |
136 | $error = $this->checkFormErrors( $user, $config ); |
137 | |
138 | if ( $error ) { |
139 | $this->getOutput()->showErrorPage( ...$error ); |
140 | return; |
141 | } |
142 | |
143 | // Set page title now we're certain we will display the form |
144 | $this->getOutput()->setPageTitleMsg( |
145 | $this->msg( $this->getFormSpecificMessageKey( 'contactpage-title' ) ) |
146 | ); |
147 | |
148 | $formItems = $this->getFormFields( $user, $config ); |
149 | |
150 | $form = HTMLForm::factory( 'ooui', |
151 | $formItems, $this->getContext(), "contactpage-{$this->formType}" |
152 | ); |
153 | $form->setWrapperLegendMsg( 'contactpage-legend' ); |
154 | $form->setSubmitTextMsg( $this->getFormSpecificMessageKey( 'emailsend' ) ); |
155 | if ( $this->formType != '' ) { |
156 | $form->setId( "contactpage-{$this->formType}" ); |
157 | |
158 | $msg = $this->msg( "contactpage-legend-{$this->formType}" ); |
159 | if ( !$msg->isDisabled() ) { |
160 | $form->setWrapperLegendMsg( $msg ); |
161 | } |
162 | |
163 | $msg = $this->msg( "contactpage-emailsend-{$this->formType}" ); |
164 | if ( !$msg->isDisabled() ) { |
165 | $form->setSubmitTextMsg( $msg ); |
166 | } |
167 | } |
168 | $form->setSubmitCallback( [ $this, 'processInput' ] ); |
169 | $form->prepareForm(); |
170 | |
171 | // Stolen from Special:EmailUser |
172 | if ( !$this->getContactPageHookRunner()->onEmailUserForm( $form ) ) { |
173 | return; |
174 | } |
175 | |
176 | $result = $form->show(); |
177 | |
178 | if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) { |
179 | $output = $this->getOutput(); |
180 | $output->setPageTitleMsg( $this->msg( $this->getFormSpecificMessageKey( 'emailsent' ) ) ); |
181 | $output->addWikiMsg( |
182 | $this->getFormSpecificMessageKey( 'emailsenttext' ), |
183 | $this->recipientName |
184 | ); |
185 | |
186 | $output->returnToMain( false ); |
187 | } else { |
188 | if ( $config['RLStyleModules'] ) { |
189 | $this->getOutput()->addModuleStyles( $config['RLStyleModules'] ); |
190 | } |
191 | if ( $config['RLModules'] ) { |
192 | $this->getOutput()->addModules( $config['RLModules'] ); |
193 | } |
194 | $formText = $this->msg( |
195 | $this->getFormSpecificMessageKey( 'contactpage-pagetext' ) |
196 | )->parseAsBlock(); |
197 | $this->getOutput()->prependHTML( trim( $formText ) ); |
198 | } |
199 | } |
200 | |
201 | /** |
202 | * Various permission and form misconfiguration checks |
203 | * |
204 | * When there's an error and the form should not displayed, the return value |
205 | * must be an array of exactly 2 string elements: message key for the error page title |
206 | * and message key for the actual error message. |
207 | * |
208 | * The method may also throw a subclass of ErrorPageError to halt displaying the form. |
209 | * |
210 | * false means there's no error and we should proceed to display the form. |
211 | * |
212 | * @return array|false |
213 | * @phan-return array{0:string,1:string}|false [ error title msg key, error text msg key ] |
214 | * @throws \UserNotLoggedIn |
215 | * @throws \UserBlockedError |
216 | */ |
217 | private function checkFormErrors( User $user, array $config ) { |
218 | // Display error if user not logged in when config requires it |
219 | $requiresConfirmedEmail = $config['MustHaveEmail'] ?? false; |
220 | $requiresLogin = $config['MustBeLoggedIn'] ?? false; |
221 | |
222 | if ( $requiresLogin ) { |
223 | // Uses the following message keys: |
224 | // * contactpage-mustbeloggedin |
225 | // * contactpage-mustbeloggedin-for-temp-user |
226 | $this->requireNamedUser( 'contactpage-mustbeloggedin' ); |
227 | } elseif ( $requiresConfirmedEmail ) { |
228 | // MustHaveEmail must not be set without setting MustBeLoggedIn, as |
229 | // anon and temporary users do not have email addresses. |
230 | return [ 'contactpage-config-error-title', 'contactpage-config-error' ]; |
231 | } |
232 | |
233 | // Display error if sender has no confirmed email when config requires it |
234 | if ( $requiresConfirmedEmail && !$user->isEmailConfirmed() ) { |
235 | return [ 'contactpage-musthaveemail-error-title', 'contactpage-musthaveemail-error' ]; |
236 | } |
237 | |
238 | // Display error if no recipient specified in configuration |
239 | if ( !$config['RecipientUser'] && !$config['RecipientEmail'] ) { |
240 | return [ 'contactpage-config-error-title', 'contactpage-config-error' ]; |
241 | } |
242 | |
243 | // Display error if 'RecipientUser' is used, but they have email disabled |
244 | if ( $config['RecipientUser'] ) { |
245 | $recipient = $this->userFactory->newFromName( $config['RecipientUser'] ); |
246 | if ( $recipient === null || !$recipient->canReceiveEmail() ) { |
247 | return [ 'noemailtitle', 'noemailtext' ]; |
248 | } |
249 | $this->recipientName = $config['RecipientUser']; |
250 | } else { |
251 | $this->recipientName = $config['RecipientName'] ?? $this->getConfig()->get( 'Sitename' ); |
252 | } |
253 | |
254 | // Blocked users cannot use the contact form if they're disabled from sending email. |
255 | $block = $user->getBlock(); |
256 | if ( $block && $block->appliesToRight( 'sendemail' ) ) { |
257 | $useCustomBlockMessage = $config['UseCustomBlockMessage'] ?? false; |
258 | if ( $useCustomBlockMessage ) { |
259 | return [ $this->getFormSpecificMessageKey( 'contactpage-title' ), |
260 | $this->getFormSpecificMessageKey( 'contactpage-blocked-message' ) ]; |
261 | } |
262 | |
263 | throw new UserBlockedError( $block ); |
264 | } |
265 | |
266 | // Show error if the following are true as they are in combination invalid configuration: |
267 | // * The form doesn't require logging in |
268 | // * The form requires details |
269 | // * The email form is read only. |
270 | // This is because the email field will be empty for anon and temp users and must be filled |
271 | // for the form to be valid, but cannot be modified by the client. |
272 | $emailReadonly = $user->isNamed() && ( $config['EmailReadonly'] ?? false ); |
273 | if ( !$requiresLogin && $emailReadonly && $config['RequireDetails'] ) { |
274 | return [ 'contactpage-config-error-title', 'contactpage-config-error' ]; |
275 | } |
276 | |
277 | return false; |
278 | } |
279 | |
280 | private function getFormFields( User $user, array $config ): array { |
281 | # Check for type in [[Special:Contact/type]]: change pagetext and prefill form fields |
282 | $formSpecificSubjectMessageKey = $this->msg( [ |
283 | 'contactpage-defsubject-' . $this->formType, |
284 | 'contactpage-subject-' . $this->formType |
285 | ] ); |
286 | |
287 | if ( $this->formType != '' && !$formSpecificSubjectMessageKey->isDisabled() ) { |
288 | $subject = trim( $formSpecificSubjectMessageKey->inContentLanguage()->plain() ); |
289 | } else { |
290 | $subject = $this->msg( 'contactpage-defsubject' )->inContentLanguage()->text(); |
291 | } |
292 | |
293 | $fromAddress = ''; |
294 | $fromName = ''; |
295 | $nameReadonly = false; |
296 | $emailReadonly = false; |
297 | $subjectReadonly = $config['SubjectReadonly'] ?? false; |
298 | |
299 | if ( $user->isRegistered() ) { |
300 | // See T335962#10283428 for an explanation for why |
301 | // we are including temporary accounts here. |
302 | $realName = $user->getRealName(); |
303 | if ( $realName ) { |
304 | $fromName = $realName; |
305 | } else { |
306 | $fromName = $user->getName(); |
307 | } |
308 | $fromAddress = $user->getEmail(); |
309 | $nameReadonly = $config['NameReadonly'] ?? false; |
310 | $emailReadonly = $config['EmailReadonly'] ?? false; |
311 | } |
312 | |
313 | $stockFields = [ |
314 | 'FromName' => [ |
315 | 'label-message' => $this->getFormSpecificMessageKey( 'contactpage-fromname' ), |
316 | 'type' => 'text', |
317 | 'required' => $config['RequireDetails'], |
318 | 'default' => $fromName, |
319 | 'disabled' => $nameReadonly, |
320 | ], |
321 | 'FromAddress' => [ |
322 | 'label-message' => $this->getFormSpecificMessageKey( 'contactpage-fromaddress' ), |
323 | 'type' => 'email', |
324 | 'required' => $config['RequireDetails'], |
325 | 'default' => $fromAddress, |
326 | 'disabled' => $emailReadonly, |
327 | ], |
328 | 'Subject' => [ |
329 | 'label-message' => $this->getFormSpecificMessageKey( 'emailsubject' ), |
330 | 'type' => 'text', |
331 | 'default' => $subject, |
332 | 'disabled' => $subjectReadonly, |
333 | ], |
334 | ]; |
335 | |
336 | // Control fields cannot be repositioned or removed by FieldsMergeStrategy |
337 | // option so as to ensure better visual hierarchy and consistent form control. |
338 | $controlFields = [ |
339 | 'CCme' => [ |
340 | 'label-message' => $this->getFormSpecificMessageKey( 'emailccme' ), |
341 | 'type' => 'check', |
342 | 'default' => $this->userOptionsLookup->getBoolOption( $user, 'ccmeonemails' ), |
343 | ], |
344 | 'FormType' => [ |
345 | 'class' => HTMLHiddenField::class, |
346 | 'label' => 'Type', |
347 | 'default' => $this->formType, |
348 | ] |
349 | ]; |
350 | |
351 | if ( $config['IncludeIP'] && $user->isRegistered() ) { |
352 | $controlFields['IncludeIP'] = [ |
353 | 'label-message' => $this->getFormSpecificMessageKey( 'contactpage-includeip' ), |
354 | 'type' => 'check', |
355 | ]; |
356 | } |
357 | |
358 | if ( $this->useCaptcha() ) { |
359 | $controlFields['Captcha'] = [ |
360 | 'label-message' => 'captcha-label', |
361 | 'type' => 'info', |
362 | 'default' => $this->getCaptcha(), |
363 | 'raw' => true, |
364 | ]; |
365 | } |
366 | |
367 | $additionalFields = $config['AdditionalFields'] ?? []; |
368 | |
369 | if ( $additionalFields && $config['FieldsMergeStrategy'] === 'replace' ) { |
370 | // Merge fields and redefine stock fields with the same key. |
371 | $formFields = $additionalFields + $stockFields; |
372 | |
373 | // Stock fields set to null should be removed from the form |
374 | $items = [ |
375 | 'FromName' => [ $fromName, $nameReadonly ], |
376 | 'FromAddress' => [ $fromAddress, $emailReadonly ], |
377 | 'Subject' => [ $subject, $subjectReadonly ], |
378 | ]; |
379 | |
380 | foreach ( $items as $field => [ $value, $disabled ] ) { |
381 | // Remove the field entirely if set to null |
382 | if ( $formFields[$field] === null ) { |
383 | unset( $formFields[$field] ); |
384 | } else { |
385 | // if 'default' is null/unset, use computed default value |
386 | $formFields[$field]['default'] ??= $value; |
387 | // if 'disabled' is null/unset, set it from form config. |
388 | $formFields[$field]['disabled'] ??= $disabled; |
389 | } |
390 | |
391 | } |
392 | } else { |
393 | $formFields = $stockFields + $additionalFields; |
394 | } |
395 | |
396 | // This field needs to be immediately after 'FromAddress' field. We have to |
397 | // do this here so as to check if that field exists and if we should add this one. |
398 | if ( isset( $formFields['FromAddress'] ) && !$config['RequireDetails'] ) { |
399 | $fromInfo = [ |
400 | 'FromInfo' => [ |
401 | 'label' => '', |
402 | 'type' => 'info', |
403 | 'default' => Html::rawElement( 'small', [], |
404 | $this->msg( |
405 | $this->getFormSpecificMessageKey( 'contactpage-formfootnotes' ) |
406 | )->escaped() |
407 | ), |
408 | 'raw' => true, |
409 | ] |
410 | ]; |
411 | |
412 | $formFields = wfArrayInsertAfter( $formFields, $fromInfo, 'FromAddress' ); |
413 | |
414 | } |
415 | |
416 | // Form controls fields cannot be overriden. Use array_merge() to enforce that. |
417 | return array_merge( $formFields, $controlFields ); |
418 | } |
419 | |
420 | /** |
421 | * @param array $formData |
422 | * @return bool|string|array|Status |
423 | * - Bool true or a good Status object indicates success, |
424 | * - Bool false indicates no submission was attempted, |
425 | * - Anything else indicates failure. The value may be a fatal Status |
426 | * object, an HTML string, or an array of arrays (message keys and |
427 | * params) or strings (message keys) |
428 | */ |
429 | public function processInput( $formData ) { |
430 | $config = $this->getTypeConfig(); |
431 | |
432 | $request = $this->getRequest(); |
433 | $user = $this->getUser(); |
434 | |
435 | if ( $this->useCaptcha() && |
436 | !$this->getConfig()->get( 'Captcha' )->passCaptchaFromRequest( $request, $user ) |
437 | ) { |
438 | return [ 'contactpage-captcha-error' ]; |
439 | } |
440 | |
441 | $senderIP = $request->getIP(); |
442 | |
443 | // Setup user that is going to receive the contact page response |
444 | if ( $config['RecipientUser'] ) { |
445 | $contactRecipientUser = $this->userFactory->newFromName( $config['RecipientUser'] ); |
446 | '@phan-var \MediaWiki\User\User $contactRecipientUser'; |
447 | $contactRecipientAddress = MailAddress::newFromUser( $contactRecipientUser ); |
448 | $ccName = $contactRecipientUser->getName(); |
449 | } else { |
450 | $ccName = $config['RecipientName'] ?? $this->getConfig()->get( 'Sitename' ); |
451 | $contactRecipientAddress = new MailAddress( $config['RecipientEmail'] ); |
452 | } |
453 | |
454 | // Used when user hasn't set an email, when $wgUserEmailUseReplyTo is true, |
455 | // or when sending CC email to user |
456 | $siteAddress = new MailAddress( |
457 | $config['SenderEmail'] ?: $this->getConfig()->get( 'PasswordSender' ), |
458 | $config['SenderName'] |
459 | ); |
460 | |
461 | // Initialize the sender to the site address |
462 | $senderAddress = $siteAddress; |
463 | |
464 | $fromAddress = $formData['FromAddress'] ?? ''; |
465 | $fromName = $formData['FromName'] ?? ''; |
466 | |
467 | $fromUserAddress = null; |
468 | $replyTo = null; |
469 | |
470 | if ( $fromAddress ) { |
471 | // T232199 - If the email address is invalid, bail out. |
472 | // Don't allow it to fallback to basically @server.host.name |
473 | if ( !Sanitizer::validateEmail( $fromAddress ) ) { |
474 | return [ 'invalidemailaddress' ]; |
475 | } |
476 | |
477 | // Use user submitted details |
478 | $fromUserAddress = new MailAddress( $fromAddress, $fromName ); |
479 | |
480 | if ( $this->getConfig()->get( 'UserEmailUseReplyTo' ) ) { |
481 | // Define reply-to address |
482 | $replyTo = $fromUserAddress; |
483 | } else { |
484 | // Not using ReplyTo, so set the sender to $fromUserAddress |
485 | $senderAddress = $fromUserAddress; |
486 | } |
487 | } |
488 | |
489 | $includeIP = isset( $config['IncludeIP'] ) && $config['IncludeIP'] |
490 | && ( $user->isAnon() || $formData['IncludeIP'] ); |
491 | $subject = $formData['Subject'] ?? ''; |
492 | |
493 | if ( $fromName !== '' ) { |
494 | if ( $includeIP ) { |
495 | $subject = $this->msg( |
496 | 'contactpage-subject-and-sender-withip', |
497 | $subject, |
498 | $fromName, |
499 | $senderIP |
500 | )->inContentLanguage()->text(); |
501 | } else { |
502 | $subject = $this->msg( |
503 | 'contactpage-subject-and-sender', |
504 | $subject, |
505 | $fromName |
506 | )->inContentLanguage()->text(); |
507 | } |
508 | } elseif ( $fromAddress !== '' ) { |
509 | if ( $includeIP ) { |
510 | $subject = $this->msg( |
511 | 'contactpage-subject-and-sender-withip', |
512 | $subject, |
513 | $fromAddress, |
514 | $senderIP |
515 | )->inContentLanguage()->text(); |
516 | } else { |
517 | $subject = $this->msg( |
518 | 'contactpage-subject-and-sender', |
519 | $subject, |
520 | $fromAddress |
521 | )->inContentLanguage()->text(); |
522 | } |
523 | } elseif ( $includeIP ) { |
524 | $subject = $this->msg( |
525 | 'contactpage-subject-and-sender', |
526 | $subject, |
527 | $senderIP |
528 | )->inContentLanguage()->text(); |
529 | } |
530 | |
531 | $text = ''; |
532 | foreach ( $config['AdditionalFields'] ?? [] as $name => $field ) { |
533 | if ( $field == null ) { |
534 | continue; |
535 | } |
536 | |
537 | $class = HTMLForm::getClassFromDescriptor( $name, $field ); |
538 | |
539 | $value = ''; |
540 | // TODO: Support selectandother/HTMLSelectAndOtherField |
541 | // options, options-messages and options-message |
542 | if ( isset( $field['options-messages'] ) ) { |
543 | // Multiple values! |
544 | if ( is_string( $formData[$name] ) ) { |
545 | $optionValues = array_flip( $field['options-messages'] ); |
546 | if ( isset( $optionValues[$formData[$name]] ) ) { |
547 | $value = $this->msg( $optionValues[$formData[$name]] )->inContentLanguage()->text(); |
548 | } else { |
549 | $value = $formData[$name]; |
550 | } |
551 | } elseif ( count( $formData[$name] ) ) { |
552 | $formValues = array_flip( $formData[$name] ); |
553 | $value .= "\n"; |
554 | foreach ( $field['options-messages'] as $msg => $optionValue ) { |
555 | $msg = $this->msg( $msg )->inContentLanguage()->text(); |
556 | $optionValue = $this->getYesOrNoMsg( isset( $formValues[$optionValue] ) ); |
557 | $value .= "\t$msg: $optionValue\n"; |
558 | } |
559 | } |
560 | } elseif ( isset( $field['options'] ) ) { |
561 | if ( is_string( $formData[$name] ) ) { |
562 | $value = $formData[$name]; |
563 | } elseif ( count( $formData[$name] ) ) { |
564 | $formValues = array_flip( $formData[$name] ); |
565 | $value .= "\n"; |
566 | foreach ( $field['options'] as $msg => $optionValue ) { |
567 | $optionValue = $this->getYesOrNoMsg( isset( $formValues[$optionValue] ) ); |
568 | $value .= "\t$msg: $optionValue\n"; |
569 | } |
570 | } |
571 | } elseif ( $class === HTMLCheckField::class |
572 | // Checking old alias for compatibility with unchanged extensions |
573 | || $class === \HTMLCheckField::class |
574 | ) { |
575 | $value = $this->getYesOrNoMsg( $formData[$name] xor |
576 | ( isset( $field['invert'] ) && $field['invert'] ) ); |
577 | } elseif ( isset( $formData[$name] ) ) { |
578 | // HTMLTextField, HTMLTextAreaField |
579 | // HTMLFloatField, HTMLIntField |
580 | |
581 | // Just dump the value if its wordy |
582 | $value = $formData[$name]; |
583 | } else { |
584 | continue; |
585 | } |
586 | |
587 | if ( isset( $field['contactpage-email-label'] ) ) { |
588 | $name = $field['contactpage-email-label']; |
589 | } elseif ( isset( $field['label-message'] ) ) { |
590 | $name = $this->msg( $field['label-message'] )->inContentLanguage()->text(); |
591 | } else { |
592 | $name = $field['label']; |
593 | } |
594 | |
595 | $text .= "{$name}: $value\n"; |
596 | } |
597 | |
598 | $hookRunner = $this->getContactPageHookRunner(); |
599 | if ( !$hookRunner->onContactForm( $contactRecipientAddress, $replyTo, $subject, |
600 | $text, $this->formType, $formData ) |
601 | ) { |
602 | // TODO: Need to do some proper error handling here |
603 | return false; |
604 | } |
605 | |
606 | wfDebug( __METHOD__ . ': sending mail from ' . $senderAddress->toString() . |
607 | ' to ' . $contactRecipientAddress->toString() . |
608 | ' replyto ' . ( $replyTo == null ? '-/-' : $replyTo->toString() ) . "\n" |
609 | ); |
610 | // @phan-suppress-next-line SecurityCheck-XSS UserMailer::send defaults to text/plain if passed a string |
611 | $mailResult = UserMailer::send( |
612 | $contactRecipientAddress, |
613 | $senderAddress, |
614 | $subject, |
615 | $text, |
616 | [ 'replyTo' => $replyTo ] |
617 | ); |
618 | |
619 | $language = $this->getLanguage(); |
620 | if ( !$mailResult->isOK() ) { |
621 | wfDebug( __METHOD__ . ': got error from UserMailer: ' . |
622 | $mailResult->getMessage( false, false, 'en' )->text() . "\n" ); |
623 | return [ $mailResult->getMessage( 'contactpage-usermailererror', false, $language ) ]; |
624 | } |
625 | |
626 | // if the user requested a copy of this mail, do this now, |
627 | // unless they are emailing themselves, in which case one copy of the message is sufficient. |
628 | if ( $formData['CCme'] && $fromUserAddress ) { |
629 | $cc_subject = $this->msg( 'emailccsubject', $ccName, $subject )->text(); |
630 | if ( $hookRunner->onContactForm( |
631 | $fromUserAddress, $senderAddress, $cc_subject, $text, $this->formType, $formData ) |
632 | ) { |
633 | wfDebug( __METHOD__ . ': sending cc mail from ' . $senderAddress->toString() . |
634 | ' to ' . $fromUserAddress->toString() . "\n" |
635 | ); |
636 | // @phan-suppress-next-line SecurityCheck-XSS UserMailer::send defaults to text/plain if passed a string |
637 | $ccResult = UserMailer::send( |
638 | $fromUserAddress, |
639 | $senderAddress, |
640 | $cc_subject, |
641 | $text, |
642 | ); |
643 | if ( !$ccResult->isOK() ) { |
644 | // At this stage, the user's CC mail has failed, but their |
645 | // original mail has succeeded. It's unlikely, but still, what to do? |
646 | // We can either show them an error, or we can say everything was fine, |
647 | // or we can say we sort of failed AND sort of succeeded. Of these options, |
648 | // simply saying there was an error is probably best. |
649 | return [ $ccResult->getMessage( 'contactpage-usermailererror', false, $language ) ]; |
650 | } |
651 | } |
652 | } |
653 | |
654 | $hookRunner->onContactFromComplete( $contactRecipientAddress, $replyTo, $subject, $text ); |
655 | |
656 | return true; |
657 | } |
658 | |
659 | /** |
660 | * @param bool $value |
661 | * @return string |
662 | */ |
663 | private function getYesOrNoMsg( $value ) { |
664 | return $this->msg( $value ? 'htmlform-yes' : 'htmlform-no' )->inContentLanguage()->text(); |
665 | } |
666 | |
667 | /** |
668 | * @return bool True if CAPTCHA should be used, false otherwise |
669 | */ |
670 | private function useCaptcha() { |
671 | $extRegistry = ExtensionRegistry::getInstance(); |
672 | if ( !$extRegistry->isLoaded( 'ConfirmEdit' ) ) { |
673 | return false; |
674 | } |
675 | $config = $this->getConfig(); |
676 | $captchaTriggers = $config->get( 'CaptchaTriggers' ); |
677 | |
678 | return $config->get( 'CaptchaClass' ) |
679 | && isset( $captchaTriggers['contactpage'] ) |
680 | && $captchaTriggers['contactpage'] |
681 | && !$this->getUser()->isAllowed( 'skipcaptcha' ); |
682 | } |
683 | |
684 | /** |
685 | * @return string CAPTCHA form HTML |
686 | */ |
687 | private function getCaptcha() { |
688 | // NOTE: make sure we have a session. May be required for CAPTCHAs to work. |
689 | SessionManager::getGlobalSession()->persist(); |
690 | |
691 | $captcha = ConfirmEditHooks::getInstance(); |
692 | $captcha->setTrigger( 'contactpage' ); |
693 | $captcha->setAction( 'contact' ); |
694 | |
695 | $formInformation = $captcha->getFormInformation(); |
696 | $formMetainfo = $formInformation; |
697 | unset( $formMetainfo['html'] ); |
698 | $captcha->addFormInformationToOutput( $this->getOutput(), $formMetainfo ); |
699 | |
700 | return '<div class="captcha">' . |
701 | $formInformation['html'] . |
702 | "</div>\n"; |
703 | } |
704 | |
705 | /** |
706 | * @return HookRunner |
707 | */ |
708 | private function getContactPageHookRunner() { |
709 | if ( !$this->contactPageHookRunner ) { |
710 | $this->contactPageHookRunner = new HookRunner( $this->getHookContainer() ); |
711 | } |
712 | return $this->contactPageHookRunner; |
713 | } |
714 | } |