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