Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
113 / 113 |
|
100.00% |
5 / 5 |
CRAP | |
100.00% |
1 / 1 |
ReportIncidentMailer | |
100.00% |
113 / 113 |
|
100.00% |
5 / 5 |
14 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
sendEmail | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
4 | |||
validateConfig | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
4 | |||
getLinkToReportedContent | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
actuallySendEmail | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\ReportIncident\Services; |
4 | |
5 | use MailAddress; |
6 | use MediaWiki\Config\ServiceOptions; |
7 | use MediaWiki\Extension\ReportIncident\IncidentReport; |
8 | use MediaWiki\Extension\ReportIncident\IncidentReportEmailStatus; |
9 | use MediaWiki\Mail\IEmailer; |
10 | use MediaWiki\Title\TitleFactory; |
11 | use MediaWiki\Utils\UrlUtils; |
12 | use Psr\Log\LoggerInterface; |
13 | use StatusValue; |
14 | use Wikimedia\Message\ITextFormatter; |
15 | use Wikimedia\Message\MessageValue; |
16 | |
17 | /** |
18 | * Handles emailing incident reports. |
19 | */ |
20 | class ReportIncidentMailer { |
21 | |
22 | private ServiceOptions $options; |
23 | private TitleFactory $titleFactory; |
24 | private ITextFormatter $textFormatter; |
25 | private IEmailer $emailer; |
26 | private LoggerInterface $logger; |
27 | private UrlUtils $urlUtils; |
28 | public const CONSTRUCTOR_OPTIONS = [ |
29 | 'ReportIncidentRecipientEmails', |
30 | 'ReportIncidentEmailFromAddress', |
31 | ]; |
32 | |
33 | /** |
34 | * @param ServiceOptions $options |
35 | * @param UrlUtils $urlUtils |
36 | * @param TitleFactory $titleFactory |
37 | * @param ITextFormatter $textFormatter |
38 | * @param IEmailer $emailer |
39 | * @param LoggerInterface $logger |
40 | */ |
41 | public function __construct( |
42 | ServiceOptions $options, |
43 | UrlUtils $urlUtils, |
44 | TitleFactory $titleFactory, |
45 | ITextFormatter $textFormatter, |
46 | IEmailer $emailer, |
47 | LoggerInterface $logger |
48 | ) { |
49 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
50 | $this->options = $options; |
51 | $this->urlUtils = $urlUtils; |
52 | $this->titleFactory = $titleFactory; |
53 | $this->textFormatter = $textFormatter; |
54 | $this->emailer = $emailer; |
55 | $this->logger = $logger; |
56 | } |
57 | |
58 | /** |
59 | * Sends an email to the administrators using the |
60 | * provided IncidentReport object as the data to |
61 | * send. |
62 | * |
63 | * @param IncidentReport $incidentReport The IncidentReport object containing the data to send in the email. |
64 | * @return IncidentReportEmailStatus The result of attempting to email the administrators. A good status indicates |
65 | * an email was sent. |
66 | */ |
67 | public function sendEmail( IncidentReport $incidentReport ): StatusValue { |
68 | $reportIncidentEmailStatus = $this->validateConfig(); |
69 | if ( !$reportIncidentEmailStatus->isGood() ) { |
70 | // Return early if the config was not valid. |
71 | return $reportIncidentEmailStatus; |
72 | } |
73 | // Get MailAddress objects for the to emails and from email. |
74 | $to = array_map( static function ( $address ) { |
75 | return new MailAddress( $address ); |
76 | }, $this->options->get( 'ReportIncidentRecipientEmails' ) ); |
77 | $from = new MailAddress( |
78 | $this->options->get( 'ReportIncidentEmailFromAddress' ), |
79 | $this->textFormatter->format( |
80 | new MessageValue( 'emailsender' ) |
81 | ) |
82 | ); |
83 | $reportingUserPage = $this->titleFactory->newFromText( |
84 | $incidentReport->getReportingUser()->getName(), NS_USER |
85 | ); |
86 | $subject = $this->textFormatter->format( |
87 | new MessageValue( |
88 | 'reportincident-email-subject', |
89 | [ $reportingUserPage->getPrefixedDBkey() ] |
90 | ) |
91 | ); |
92 | |
93 | [ $linkPrefixText, $linkToPageAtRevision ] = $this->getLinkToReportedContent( $incidentReport ); |
94 | |
95 | $reportedUserPage = $this->titleFactory->newFromText( |
96 | $incidentReport->getReportedUser()->getName(), NS_USER |
97 | ); |
98 | // Get the behaviors and substitute the 'something-else' behavior |
99 | // with the text submitted in the Something else textbox. |
100 | $behaviors = $incidentReport->getBehaviors(); |
101 | if ( $incidentReport->getSomethingElseDetails() ) { |
102 | $somethingElseIndex = array_search( 'something-else', $behaviors ); |
103 | if ( $somethingElseIndex !== false ) { |
104 | $behaviors[$somethingElseIndex] = $this->textFormatter->format( |
105 | new MessageValue( |
106 | 'reportincident-email-something-else', |
107 | [ $incidentReport->getSomethingElseDetails() ] |
108 | ) |
109 | ); |
110 | } |
111 | } |
112 | $emailUrl = $this->titleFactory->newFromText( 'Special:EmailUser' ) |
113 | ->getSubpage( $reportingUserPage->getDBkey() ) |
114 | ->getFullURL(); |
115 | |
116 | $body = $this->textFormatter->format( |
117 | new MessageValue( |
118 | 'reportincident-email-body', |
119 | [ |
120 | $reportingUserPage->getDBkey(), |
121 | $reportedUserPage->getDBkey(), |
122 | $linkPrefixText, |
123 | $linkToPageAtRevision, |
124 | implode( ', ', $behaviors ), |
125 | $incidentReport->getDetails(), |
126 | $emailUrl |
127 | ] |
128 | ) |
129 | ); |
130 | return $this->actuallySendEmail( $to, $from, $subject, $body, $reportIncidentEmailStatus ); |
131 | } |
132 | |
133 | /** |
134 | * Validates the configuration settings wgReportIncidentRecipientEmails |
135 | * and wgReportIncidentEmailFromAddress. If invalid this method returns |
136 | * a fatal status. Otherwise a good status is returned. |
137 | * |
138 | * |
139 | * @return IncidentReportEmailStatus |
140 | */ |
141 | private function validateConfig(): IncidentReportEmailStatus { |
142 | $recipientEmails = $this->options->get( 'ReportIncidentRecipientEmails' ); |
143 | if ( !$recipientEmails || !is_array( $recipientEmails ) ) { |
144 | $this->logger->error( |
145 | 'ReportIncidentRecipientEmails configuration is empty or not an array, not sending an email.' |
146 | ); |
147 | return IncidentReportEmailStatus::newFatal( |
148 | 'rawmessage', |
149 | 'ReportIncidentRecipientEmails configuration is empty or not an array, not sending an email.' |
150 | ); |
151 | } |
152 | if ( !$this->options->get( 'ReportIncidentEmailFromAddress' ) ) { |
153 | $this->logger->error( 'ReportIncidentEmailFromAddress configuration is empty, not sending an email.' ); |
154 | return IncidentReportEmailStatus::newFatal( |
155 | 'rawmessage', |
156 | 'ReportIncidentEmailFromAddress configuration is empty, not sending an email.' |
157 | ); |
158 | } |
159 | return IncidentReportEmailStatus::newGood(); |
160 | } |
161 | |
162 | /** |
163 | * Gets a link to the reported content. This returns a link to |
164 | * the permalink of the page, and if the report entry point was |
165 | * via DiscussionTools the link includes an anchor to the topic |
166 | * or comment that was reported. |
167 | * |
168 | * @param IncidentReport $incidentReport The IncidentReport object provided to ::sendEmail |
169 | * @return array First item being the prefix text for the link to be used in the email |
170 | * and the second item being the URL to the reported content. |
171 | */ |
172 | private function getLinkToReportedContent( IncidentReport $incidentReport ): array { |
173 | $revision = $incidentReport->getRevisionRecord(); |
174 | // In theory UrlUtils::expand() could return null, this seems pretty unlikely in practice; |
175 | // cast to string to make Phan happy. |
176 | $entrypointUrl = (string)$this->urlUtils->expand( wfScript() ); |
177 | $linkToPageAtRevision = wfAppendQuery( |
178 | $entrypointUrl, |
179 | [ 'oldid' => $revision->getId() ] |
180 | ); |
181 | $threadId = $incidentReport->getThreadId(); |
182 | if ( $threadId ) { |
183 | // If a thread ID is defined, then add it to the link to the page at revision |
184 | // as an anchor in the URL. Currently this is only provided by DiscussionTools. |
185 | $linkToPageAtRevision .= '#' . urlencode( $threadId ); |
186 | // If the threadId starts with 'h-', then the threadId refers to a topic/section header |
187 | // and as such the link prefix text should indicate this instead of saying it is a comment. |
188 | if ( substr( $threadId, 0, 2 ) === 'h-' ) { |
189 | $linkPrefixText = new MessageValue( 'reportincident-email-link-to-topic-prefix' ); |
190 | } else { |
191 | $linkPrefixText = new MessageValue( 'reportincident-email-link-to-comment-prefix' ); |
192 | } |
193 | } else { |
194 | $linkPrefixText = new MessageValue( 'reportincident-email-link-to-page-prefix' ); |
195 | } |
196 | return [ $linkPrefixText, $linkToPageAtRevision ]; |
197 | } |
198 | |
199 | /** |
200 | * Actually sends the email when provided with the |
201 | * 'to' email address, 'from' email addresses, the subject, |
202 | * and the body of the email. |
203 | * |
204 | * @param MailAddress[] $to |
205 | * @param MailAddress $from |
206 | * @param string $subject |
207 | * @param string $body |
208 | * @param IncidentReportEmailStatus $incidentReportEmailStatus Should be a status with no errors. |
209 | * @return IncidentReportEmailStatus |
210 | */ |
211 | private function actuallySendEmail( |
212 | array $to, |
213 | MailAddress $from, |
214 | string $subject, |
215 | string $body, |
216 | IncidentReportEmailStatus $incidentReportEmailStatus |
217 | ) { |
218 | // Call IEmailer::send and merge the status returned with the |
219 | // existing $incidentReportEmailStatus status. |
220 | $incidentReportEmailStatus->merge( |
221 | $this->emailer->send( |
222 | $to, |
223 | $from, |
224 | $subject, |
225 | $body |
226 | ), |
227 | true |
228 | ); |
229 | // Add the email contents to $incidentReportEmailStatus |
230 | $incidentReportEmailStatus->emailContents = [ |
231 | 'to' => $to, |
232 | 'from' => $from, |
233 | 'subject' => $subject, |
234 | 'body' => $body, |
235 | ]; |
236 | if ( !$incidentReportEmailStatus->isGood() ) { |
237 | // Log an error if the IEmailer::send method returns a fatal status. |
238 | $this->logger->error( |
239 | 'Unable to send report incident email. IEmailer::send returned the following: {emailer-message}', |
240 | [ |
241 | 'emailer-message' => $incidentReportEmailStatus->getMessage( false, false, 'en' ), |
242 | ] |
243 | ); |
244 | } |
245 | return $incidentReportEmailStatus; |
246 | } |
247 | } |