Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
13.69% |
36 / 263 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
EmailNotification | |
13.69% |
36 / 263 |
|
0.00% |
0 / 9 |
3691.87 | |
0.00% |
0 / 1 |
getPageStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
notifyOnPageChange | |
92.31% |
36 / 39 |
|
0.00% |
0 / 1 |
14.09 | |||
actuallyNotifyOnPageChange | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
506 | |||
canSendUserTalkEmail | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
210 | |||
composeCommonMailtext | |
0.00% |
0 / 78 |
|
0.00% |
0 / 1 |
182 | |||
compose | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
sendMails | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
sendPersonalised | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
20 | |||
sendImpersonal | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @author Brooke Vibber |
20 | * @author <mail@tgries.de> |
21 | * @author Tim Starling |
22 | * @author Luke Welling lwelling@wikimedia.org |
23 | */ |
24 | |
25 | use MediaWiki\HookContainer\HookRunner; |
26 | use MediaWiki\Mail\UserEmailContact; |
27 | use MediaWiki\MainConfigNames; |
28 | use MediaWiki\MediaWikiServices; |
29 | use MediaWiki\Permissions\Authority; |
30 | use MediaWiki\SpecialPage\SpecialPage; |
31 | use MediaWiki\Title\Title; |
32 | use MediaWiki\User\User; |
33 | use MediaWiki\User\UserArray; |
34 | use MediaWiki\User\UserIdentity; |
35 | |
36 | /** |
37 | * Find watchers and create email notifications after a page is changed. |
38 | * |
39 | * After an edit is published to RCFeed, RecentChange::save calls EmailNotification. |
40 | * Here we query the `watchlist` table (via WatchedItemStore) to find who is watching |
41 | * a given page, format the emails in question, and dispatch emails to each of them |
42 | * via the JobQueue. |
43 | * |
44 | * The current implementation sends independent emails to each watching user for |
45 | * the following reason: Each email mentions the page edit time expressed in |
46 | * the person's local time (UTC is shown additionally). To achieve this, we need to |
47 | * find the individual timeoffset of each watching user from the preferences. |
48 | * |
49 | * Visit the documentation pages under |
50 | * https://www.mediawiki.org/wiki/Help:Watching_pages |
51 | * |
52 | * @todo If the volume becomes too great, we could send out bulk mails (bcc:user1,user2...) |
53 | * grouped by users having the same timeoffset in their preferences. This would however |
54 | * need to carefully consider impact of failure rate, re-try behaviour, and idempotence. |
55 | * |
56 | * @todo Use UserOptionsLookup and other services, consider converting this to a service |
57 | * |
58 | * @since 1.11.0 |
59 | * @ingroup Mail |
60 | */ |
61 | class EmailNotification { |
62 | |
63 | /** |
64 | * Notification is due to user's user talk being edited |
65 | */ |
66 | private const USER_TALK = 'user_talk'; |
67 | /** |
68 | * Notification is due to a watchlisted page being edited |
69 | */ |
70 | private const WATCHLIST = 'watchlist'; |
71 | /** |
72 | * Notification because user is notified for all changes |
73 | */ |
74 | private const ALL_CHANGES = 'all_changes'; |
75 | |
76 | protected string $subject = ''; |
77 | |
78 | protected string $body = ''; |
79 | |
80 | protected ?MailAddress $replyto; |
81 | |
82 | protected ?MailAddress $from; |
83 | |
84 | protected ?string $timestamp; |
85 | |
86 | protected string $summary = ''; |
87 | |
88 | protected ?bool $minorEdit; |
89 | |
90 | /** @var int|null|bool */ |
91 | protected $oldid; |
92 | |
93 | protected bool $composed_common = false; |
94 | |
95 | protected string $pageStatus = ''; |
96 | |
97 | /** @var MailAddress[] */ |
98 | protected array $mailTargets = []; |
99 | |
100 | protected Title $title; |
101 | |
102 | protected User $editor; |
103 | |
104 | /** |
105 | * Extensions that have hooks for |
106 | * UpdateUserMailerFormattedPageStatus (to provide additional |
107 | * pageStatus indicators) need a way to make sure that, when their |
108 | * hook is called in SendWatchlistemailNotification, they only |
109 | * handle notifications using their pageStatus indicator. |
110 | * |
111 | * @since 1.33 |
112 | * @return string |
113 | */ |
114 | public function getPageStatus() { |
115 | return $this->pageStatus; |
116 | } |
117 | |
118 | /** |
119 | * Send emails corresponding to the user $editor editing the page $title. |
120 | * |
121 | * May be deferred via the job queue. |
122 | * |
123 | * @since 1.11.0 |
124 | * @since 1.35 returns a boolean indicating whether an email job was created. |
125 | * @param Authority $editor |
126 | * @param Title $title |
127 | * @param string $timestamp |
128 | * @param string $summary |
129 | * @param bool $minorEdit |
130 | * @param bool $oldid (default: false) |
131 | * @param string $pageStatus (default: 'changed') |
132 | * @return bool Whether an email job was created or not. |
133 | */ |
134 | public function notifyOnPageChange( |
135 | Authority $editor, |
136 | $title, |
137 | $timestamp, |
138 | $summary, |
139 | $minorEdit, |
140 | $oldid = false, |
141 | $pageStatus = 'changed' |
142 | ): bool { |
143 | if ( $title->getNamespace() < 0 ) { |
144 | return false; |
145 | } |
146 | |
147 | $mwServices = MediaWikiServices::getInstance(); |
148 | $config = $mwServices->getMainConfig(); |
149 | |
150 | // update wl_notificationtimestamp for watchers |
151 | $watchers = []; |
152 | if ( $config->get( MainConfigNames::EnotifWatchlist ) || $config->get( MainConfigNames::ShowUpdatedMarker ) ) { |
153 | $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp( |
154 | $editor->getUser(), |
155 | $title, |
156 | $timestamp |
157 | ); |
158 | } |
159 | |
160 | // Don't send email for bots |
161 | if ( $mwServices->getUserFactory()->newFromAuthority( $editor )->isBot() ) { |
162 | return false; |
163 | } |
164 | |
165 | $sendEmail = true; |
166 | // $watchers deals with $wgEnotifWatchlist. |
167 | // If nobody is watching the page, and there are no users notified on all changes |
168 | // don't bother creating a job/trying to send emails, unless it's a |
169 | // talk page with an applicable notification. |
170 | if ( $watchers === [] && |
171 | !count( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) ) |
172 | ) { |
173 | $sendEmail = false; |
174 | // Only send notification for non minor edits, unless $wgEnotifMinorEdits |
175 | if ( !$minorEdit || |
176 | ( $config->get( MainConfigNames::EnotifMinorEdits ) && |
177 | !$editor->isAllowed( 'nominornewtalk' ) ) |
178 | ) { |
179 | if ( $config->get( MainConfigNames::EnotifUserTalk ) |
180 | && $title->getNamespace() === NS_USER_TALK |
181 | && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit ) |
182 | ) { |
183 | $sendEmail = true; |
184 | } |
185 | } |
186 | } |
187 | |
188 | if ( $sendEmail ) { |
189 | $mwServices->getJobQueueGroup()->lazyPush( new EnotifNotifyJob( |
190 | $title, |
191 | [ |
192 | 'editor' => $editor->getUser()->getName(), |
193 | 'editorID' => $editor->getUser()->getId(), |
194 | 'timestamp' => $timestamp, |
195 | 'summary' => $summary, |
196 | 'minorEdit' => $minorEdit, |
197 | 'oldid' => $oldid, |
198 | 'watchers' => $watchers, |
199 | 'pageStatus' => $pageStatus |
200 | ] |
201 | ) ); |
202 | } |
203 | |
204 | return $sendEmail; |
205 | } |
206 | |
207 | /** |
208 | * Immediate version of notifyOnPageChange(). |
209 | * |
210 | * Send emails corresponding to the user $editor editing the page $title. |
211 | * |
212 | * @note Do not call directly. Use notifyOnPageChange so that wl_notificationtimestamp is updated. |
213 | * |
214 | * @since 1.11.0 |
215 | * @param Authority $editor |
216 | * @param Title $title |
217 | * @param string $timestamp Edit timestamp |
218 | * @param string $summary Edit summary |
219 | * @param bool $minorEdit |
220 | * @param int $oldid Revision ID |
221 | * @param array $watchers Array of user IDs |
222 | * @param string $pageStatus |
223 | */ |
224 | public function actuallyNotifyOnPageChange( |
225 | Authority $editor, |
226 | $title, |
227 | $timestamp, |
228 | $summary, |
229 | $minorEdit, |
230 | $oldid, |
231 | $watchers, |
232 | $pageStatus = 'changed' |
233 | ) { |
234 | # we use $wgPasswordSender as sender's address |
235 | |
236 | $mwServices = MediaWikiServices::getInstance(); |
237 | $messageCache = $mwServices->getMessageCache(); |
238 | $config = $mwServices->getMainConfig(); |
239 | |
240 | # The following code is only run, if several conditions are met: |
241 | # 1. EmailNotification for pages (other than user_talk pages) must be enabled |
242 | # 2. minor edits (changes) are only regarded if the global flag indicates so |
243 | |
244 | $this->title = $title; |
245 | $this->timestamp = $timestamp; |
246 | $this->summary = $summary; |
247 | $this->minorEdit = $minorEdit; |
248 | $this->oldid = $oldid; |
249 | $this->editor = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $editor ); |
250 | $this->composed_common = false; |
251 | $this->pageStatus = $pageStatus; |
252 | |
253 | $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ]; |
254 | |
255 | $hookRunner = new HookRunner( $mwServices->getHookContainer() ); |
256 | $hookRunner->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus ); |
257 | if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) { |
258 | throw new UnexpectedValueException( 'Not a valid page status!' ); |
259 | } |
260 | |
261 | $userTalkId = false; |
262 | |
263 | if ( !$minorEdit || |
264 | ( $config->get( MainConfigNames::EnotifMinorEdits ) && |
265 | !$editor->isAllowed( 'nominornewtalk' ) ) |
266 | ) { |
267 | if ( $config->get( MainConfigNames::EnotifUserTalk ) |
268 | && $title->getNamespace() === NS_USER_TALK |
269 | && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit ) |
270 | ) { |
271 | $targetUser = User::newFromName( $title->getText() ); |
272 | $this->compose( $targetUser, self::USER_TALK, $messageCache ); |
273 | $userTalkId = $targetUser->getId(); |
274 | } |
275 | |
276 | if ( $config->get( MainConfigNames::EnotifWatchlist ) ) { |
277 | $userOptionsLookup = $mwServices->getUserOptionsLookup(); |
278 | // Send updates to watchers other than the current editor |
279 | // and don't send to watchers who are blocked and cannot login |
280 | $userArray = UserArray::newFromIDs( $watchers ); |
281 | foreach ( $userArray as $watchingUser ) { |
282 | if ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' ) |
283 | && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, 'enotifminoredits' ) ) |
284 | && $watchingUser->isEmailConfirmed() |
285 | && $watchingUser->getId() != $userTalkId |
286 | && !in_array( $watchingUser->getName(), |
287 | $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) ) |
288 | // @TODO Partial blocks should not prevent the user from logging in. |
289 | // see: https://phabricator.wikimedia.org/T208895 |
290 | && !( $config->get( MainConfigNames::BlockDisablesLogin ) && |
291 | $watchingUser->getBlock() ) |
292 | && $hookRunner->onSendWatchlistEmailNotification( $watchingUser, $title, $this ) |
293 | ) { |
294 | $this->compose( $watchingUser, self::WATCHLIST, $messageCache ); |
295 | } |
296 | } |
297 | } |
298 | } |
299 | |
300 | foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) { |
301 | if ( $editor->getUser()->getName() == $name ) { |
302 | // No point notifying the user that actually made the change! |
303 | continue; |
304 | } |
305 | $user = User::newFromName( $name ); |
306 | if ( $user instanceof User ) { |
307 | $this->compose( $user, self::ALL_CHANGES, $messageCache ); |
308 | } |
309 | } |
310 | $this->sendMails(); |
311 | } |
312 | |
313 | /** |
314 | * @param UserIdentity $editor |
315 | * @param Title $title |
316 | * @param bool $minorEdit |
317 | * @return bool |
318 | */ |
319 | private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) { |
320 | $services = MediaWikiServices::getInstance(); |
321 | $config = $services->getMainConfig(); |
322 | |
323 | if ( !$config->get( MainConfigNames::EnotifUserTalk ) || $title->getNamespace() !== NS_USER_TALK ) { |
324 | return false; |
325 | } |
326 | |
327 | $userOptionsLookup = $services->getUserOptionsLookup(); |
328 | $targetUser = User::newFromName( $title->getText() ); |
329 | |
330 | if ( !$targetUser || $targetUser->isAnon() ) { |
331 | wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" ); |
332 | } elseif ( $targetUser->getId() == $editor->getId() ) { |
333 | wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" ); |
334 | } elseif ( $targetUser->isTemp() ) { |
335 | wfDebug( __METHOD__ . ": talk page owner is a temporary user so doesn't have email" ); |
336 | } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) && |
337 | $targetUser->getBlock() |
338 | ) { |
339 | // @TODO Partial blocks should not prevent the user from logging in. |
340 | // see: https://phabricator.wikimedia.org/T208895 |
341 | wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" ); |
342 | } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' ) |
343 | && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) ) |
344 | ) { |
345 | if ( !$targetUser->isEmailConfirmed() ) { |
346 | wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" ); |
347 | } elseif ( !( new HookRunner( $services->getHookContainer() ) ) |
348 | ->onAbortTalkPageEmailNotification( $targetUser, $title ) |
349 | ) { |
350 | wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" ); |
351 | } else { |
352 | wfDebug( __METHOD__ . ": sending talk page update notification" ); |
353 | return true; |
354 | } |
355 | } else { |
356 | wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" ); |
357 | } |
358 | return false; |
359 | } |
360 | |
361 | /** |
362 | * Generate the generic "this page has been changed" e-mail text. |
363 | * @param MessageCache $messageCache |
364 | */ |
365 | private function composeCommonMailtext( MessageCache $messageCache ) { |
366 | $services = MediaWikiServices::getInstance(); |
367 | $config = $services->getMainConfig(); |
368 | $userOptionsLookup = $services->getUserOptionsLookup(); |
369 | $urlUtils = $services->getUrlUtils(); |
370 | |
371 | $this->composed_common = true; |
372 | |
373 | # You as the WikiAdmin and Sysops can make use of plenty of |
374 | # named variables when composing your notification emails while |
375 | # simply editing the Meta pages |
376 | |
377 | $keys = []; |
378 | $postTransformKeys = []; |
379 | $pageTitleUrl = $this->title->getCanonicalURL(); |
380 | $pageTitle = $this->title->getPrefixedText(); |
381 | |
382 | if ( $this->oldid ) { |
383 | // Always show a link to the diff which triggered the mail. See T34210. |
384 | $keys['$NEWPAGE'] = "\n\n" . wfMessage( |
385 | 'enotif_lastdiff', |
386 | $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] ) |
387 | )->inContentLanguage()->text(); |
388 | |
389 | if ( !$config->get( MainConfigNames::EnotifImpersonal ) ) { |
390 | // For personal mail, also show a link to the diff of all changes |
391 | // since last visited. |
392 | $keys['$NEWPAGE'] .= "\n\n" . wfMessage( |
393 | 'enotif_lastvisited', |
394 | $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] ) |
395 | )->inContentLanguage()->text(); |
396 | } |
397 | $keys['$OLDID'] = $this->oldid; |
398 | $keys['$PAGELOG'] = ''; |
399 | } else { |
400 | // If there is no revision to link to, link to the page log, which should have details. See T115183. |
401 | $keys['$OLDID'] = ''; |
402 | $keys['$NEWPAGE'] = ''; |
403 | $keys['$PAGELOG'] = "\n\n" . wfMessage( |
404 | 'enotif_pagelog', |
405 | SpecialPage::getTitleFor( 'Log' )->getCanonicalURL( [ 'page' => $this->title->getPrefixedDBkey() ] ) |
406 | )->inContentLanguage()->text(); |
407 | |
408 | } |
409 | |
410 | $keys['$PAGETITLE'] = $this->title->getPrefixedText(); |
411 | $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL(); |
412 | $keys['$PAGEMINOREDIT'] = $this->minorEdit ? |
413 | "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() : |
414 | ''; |
415 | $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' ); |
416 | |
417 | if ( $this->editor->isAnon() ) { |
418 | # real anon (user:xxx.xxx.xxx.xxx) |
419 | $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() ) |
420 | ->inContentLanguage()->text(); |
421 | $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text(); |
422 | } elseif ( $this->editor->isTemp() ) { |
423 | $keys['$PAGEEDITOR'] = wfMessage( 'enotif_temp_editor', $this->editor->getName() ) |
424 | ->inContentLanguage()->text(); |
425 | $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text(); |
426 | } else { |
427 | $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) && |
428 | $this->editor->getRealName() !== '' |
429 | ? $this->editor->getRealName() : $this->editor->getName(); |
430 | $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() ); |
431 | $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL(); |
432 | } |
433 | |
434 | $keys['$PAGEEDITOR_WIKI'] = $this->editor->getTalkPage()->getCanonicalURL(); |
435 | $keys['$HELPPAGE'] = $urlUtils->expand( |
436 | Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() ), |
437 | PROTO_CURRENT |
438 | ) ?? false; |
439 | |
440 | # Replace this after transforming the message, T37019 |
441 | $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary; |
442 | |
443 | // Now build message's subject and body |
444 | |
445 | // Messages: |
446 | // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved, |
447 | // enotif_subject_restored, enotif_subject_changed |
448 | $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage() |
449 | ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text(); |
450 | |
451 | // Messages: |
452 | // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved, |
453 | // enotif_body_intro_restored, enotif_body_intro_changed |
454 | $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus ) |
455 | ->inContentLanguage() |
456 | ->params( $pageTitle, $keys['$PAGEEDITOR'], "<{$pageTitleUrl}>" ) |
457 | ->text(); |
458 | |
459 | $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain(); |
460 | $body = strtr( $body, $keys ); |
461 | $body = $messageCache->transform( $body, false, null, $this->title ); |
462 | $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 ); |
463 | |
464 | # Reveal the page editor's address as REPLY-TO address only if |
465 | # the user has not opted-out and the option is enabled at the |
466 | # global configuration level. |
467 | $adminAddress = new MailAddress( |
468 | $config->get( MainConfigNames::PasswordSender ), |
469 | wfMessage( 'emailsender' )->inContentLanguage()->text() |
470 | ); |
471 | if ( $config->get( MainConfigNames::EnotifRevealEditorAddress ) |
472 | && ( $this->editor->getEmail() != '' ) |
473 | && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' ) |
474 | ) { |
475 | $editorAddress = MailAddress::newFromUser( $this->editor ); |
476 | if ( $config->get( MainConfigNames::EnotifFromEditor ) ) { |
477 | $this->from = $editorAddress; |
478 | } else { |
479 | $this->from = $adminAddress; |
480 | $this->replyto = $editorAddress; |
481 | } |
482 | } else { |
483 | $this->from = $adminAddress; |
484 | $this->replyto = new MailAddress( |
485 | $config->get( MainConfigNames::NoReplyAddress ) |
486 | ); |
487 | } |
488 | } |
489 | |
490 | /** |
491 | * Compose a mail to a given user and either queue it for sending, or send it now, |
492 | * depending on settings. |
493 | * |
494 | * Call sendMails() to send any mails that were queued. |
495 | * @param UserEmailContact $user |
496 | * @param string $source |
497 | * @param MessageCache $messageCache |
498 | */ |
499 | private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) { |
500 | if ( !$this->composed_common ) { |
501 | $this->composeCommonMailtext( $messageCache ); |
502 | } |
503 | |
504 | if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) { |
505 | $this->mailTargets[] = MailAddress::newFromUser( $user ); |
506 | } else { |
507 | $this->sendPersonalised( $user, $source ); |
508 | } |
509 | } |
510 | |
511 | /** |
512 | * Send any queued mails |
513 | */ |
514 | private function sendMails() { |
515 | if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) { |
516 | $this->sendImpersonal( $this->mailTargets ); |
517 | } |
518 | } |
519 | |
520 | /** |
521 | * Does the per-user customizations to a notification e-mail (name, |
522 | * timestamp in proper timezone, etc) and sends it out. |
523 | * Returns Status if email was sent successfully or not (Status::newGood() |
524 | * or Status::newFatal() respectively). |
525 | * |
526 | * @param UserEmailContact $watchingUser |
527 | * @param string $source |
528 | * @return StatusValue |
529 | */ |
530 | private function sendPersonalised( UserEmailContact $watchingUser, $source ): StatusValue { |
531 | // From the PHP manual: |
532 | // Note: The to parameter cannot be an address in the form of |
533 | // "Something <someone@example.com>". The mail command will not parse |
534 | // this properly while talking with the MTA. |
535 | $to = MailAddress::newFromUser( $watchingUser ); |
536 | |
537 | # $PAGEEDITDATE is the time and date of the page change |
538 | # expressed in terms of individual local time of the notification |
539 | # recipient, i.e. watching user |
540 | $mwServices = MediaWikiServices::getInstance(); |
541 | $contLang = $mwServices->getContentLanguage(); |
542 | $watchingUserName = ( |
543 | $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) && |
544 | $watchingUser->getRealName() !== '' |
545 | ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName(); |
546 | $body = str_replace( |
547 | [ |
548 | '$WATCHINGUSERNAME', |
549 | '$PAGEEDITDATE', |
550 | '$PAGEEDITTIME' |
551 | ], |
552 | [ |
553 | $watchingUserName, |
554 | $contLang->userDate( $this->timestamp, $watchingUser->getUser() ), |
555 | $contLang->userTime( $this->timestamp, $watchingUser->getUser() ) |
556 | ], |
557 | $this->body |
558 | ); |
559 | |
560 | $headers = []; |
561 | if ( $source === self::WATCHLIST ) { |
562 | $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist'; |
563 | } |
564 | |
565 | return $mwServices |
566 | ->getEmailer() |
567 | ->send( |
568 | [ $to ], |
569 | $this->from, |
570 | $this->subject, |
571 | $body, |
572 | null, |
573 | [ |
574 | 'replyTo' => $this->replyto, |
575 | 'headers' => $headers, |
576 | ] |
577 | ); |
578 | } |
579 | |
580 | /** |
581 | * Same as sendPersonalised but does impersonal mail suitable for bulk |
582 | * mailing. Takes an array of MailAddress objects. |
583 | * @param MailAddress[] $addresses |
584 | * @return ?StatusValue |
585 | */ |
586 | private function sendImpersonal( array $addresses ): ?StatusValue { |
587 | if ( count( $addresses ) === 0 ) { |
588 | return null; |
589 | } |
590 | $services = MediaWikiServices::getInstance(); |
591 | $contLang = $services->getContentLanguage(); |
592 | $body = str_replace( |
593 | [ |
594 | '$WATCHINGUSERNAME', |
595 | '$PAGEEDITDATE', |
596 | '$PAGEEDITTIME' |
597 | ], |
598 | [ |
599 | wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(), |
600 | $contLang->date( $this->timestamp, false, false ), |
601 | $contLang->time( $this->timestamp, false, false ) |
602 | ], |
603 | $this->body |
604 | ); |
605 | |
606 | return $services |
607 | ->getEmailer() |
608 | ->send( |
609 | $addresses, |
610 | $this->from, |
611 | $this->subject, |
612 | $body, |
613 | null, |
614 | [ |
615 | 'replyTo' => $this->replyto, |
616 | ] |
617 | ); |
618 | } |
619 | |
620 | } |