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