MediaWiki REL1_35
EmailNotification.php
Go to the documentation of this file.
1<?php
28
49
53 private const USER_TALK = 'user_talk';
57 private const WATCHLIST = 'watchlist';
61 private const ALL_CHANGES = 'all_changes';
62
63 protected $subject, $body, $replyto, $from;
64 protected $timestamp, $summary, $minorEdit, $oldid, $composed_common, $pageStatus;
65 protected $mailTargets = [];
66
70 protected $title;
71
75 protected $editor;
76
87 public function getPageStatus() {
88 return $this->pageStatus;
89 }
90
106 public function notifyOnPageChange( $editor, $title, $timestamp, $summary,
107 $minorEdit, $oldid = false, $pageStatus = 'changed'
108 ): bool {
110
111 if ( $title->getNamespace() < 0 ) {
112 return false;
113 }
114
115 // update wl_notificationtimestamp for watchers
116 $config = RequestContext::getMain()->getConfig();
117 $watchers = [];
118 if ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) {
119 $watchers = MediaWikiServices::getInstance()->getWatchedItemStore()->updateNotificationTimestamp(
120 $editor,
121 $title,
122 $timestamp
123 );
124 }
125
126 $sendEmail = true;
127 // $watchers deals with $wgEnotifWatchlist.
128 // If nobody is watching the page, and there are no users notified on all changes
129 // don't bother creating a job/trying to send emails, unless it's a
130 // talk page with an applicable notification.
131 if ( $watchers === [] && !count( $wgUsersNotifiedOnAllChanges ) ) {
132 $sendEmail = false;
133 // Only send notification for non minor edits, unless $wgEnotifMinorEdits
134 if ( !$minorEdit || ( $wgEnotifMinorEdits && !MediaWikiServices::getInstance()
136 ->userHasRight( $editor, 'nominornewtalk' ) )
137 ) {
138 $isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
140 && $isUserTalkPage
141 && $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
142 ) {
143 $sendEmail = true;
144 }
145 }
146 }
147
148 if ( $sendEmail ) {
150 $title,
151 [
152 'editor' => $editor->getName(),
153 'editorID' => $editor->getId(),
154 'timestamp' => $timestamp,
155 'summary' => $summary,
156 'minorEdit' => $minorEdit,
157 'oldid' => $oldid,
158 'watchers' => $watchers,
159 'pageStatus' => $pageStatus
160 ]
161 ) );
162 }
163
164 return $sendEmail;
165 }
166
184 $editor,
185 $title,
186 $timestamp,
187 $summary,
188 $minorEdit,
189 $oldid,
190 $watchers,
191 $pageStatus = 'changed'
192 ) {
193 # we use $wgPasswordSender as sender's address
197
198 $messageCache = MediaWikiServices::getInstance()->getMessageCache();
199
200 # The following code is only run, if several conditions are met:
201 # 1. EmailNotification for pages (other than user_talk pages) must be enabled
202 # 2. minor edits (changes) are only regarded if the global flag indicates so
203
204 $isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
205
206 $this->title = $title;
207 $this->timestamp = $timestamp;
208 $this->summary = $summary;
209 $this->minorEdit = $minorEdit;
210 $this->oldid = $oldid;
211 $this->editor = $editor;
212 $this->composed_common = false;
213 $this->pageStatus = $pageStatus;
214
215 $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
216
217 Hooks::runner()->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
218 if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
219 throw new MWException( 'Not a valid page status!' );
220 }
221
222 $userTalkId = false;
223
224 if ( !$minorEdit || ( $wgEnotifMinorEdits && !MediaWikiServices::getInstance()
226 ->userHasRight( $editor, 'nominornewtalk' ) )
227 ) {
229 && $isUserTalkPage
230 && $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
231 ) {
232 $targetUser = User::newFromName( $title->getText() );
233 $this->compose( $targetUser, self::USER_TALK, $messageCache );
234 $userTalkId = $targetUser->getId();
235 }
236
237 if ( $wgEnotifWatchlist ) {
238 // Send updates to watchers other than the current editor
239 // and don't send to watchers who are blocked and cannot login
240 $userArray = UserArray::newFromIDs( $watchers );
241 foreach ( $userArray as $watchingUser ) {
242 if ( $watchingUser->getOption( 'enotifwatchlistpages' )
243 && ( !$minorEdit || $watchingUser->getOption( 'enotifminoredits' ) )
244 && $watchingUser->isEmailConfirmed()
245 && $watchingUser->getId() != $userTalkId
246 && !in_array( $watchingUser->getName(), $wgUsersNotifiedOnAllChanges )
247 // @TODO Partial blocks should not prevent the user from logging in.
248 // see: https://phabricator.wikimedia.org/T208895
249 && !( $wgBlockDisablesLogin && $watchingUser->getBlock() )
250 && Hooks::runner()->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
251 ) {
252 $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
253 }
254 }
255 }
256 }
257
258 foreach ( $wgUsersNotifiedOnAllChanges as $name ) {
259 if ( $editor->getName() == $name ) {
260 // No point notifying the user that actually made the change!
261 continue;
262 }
263 $user = User::newFromName( $name );
264 $this->compose( $user, self::ALL_CHANGES, $messageCache );
265 }
266
267 $this->sendMails();
268 }
269
276 private function canSendUserTalkEmail( $editor, $title, $minorEdit ) {
278 $isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
279
280 if ( $wgEnotifUserTalk && $isUserTalkPage ) {
281 $targetUser = User::newFromName( $title->getText() );
282
283 if ( !$targetUser || $targetUser->isAnon() ) {
284 wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
285 } elseif ( $targetUser->getId() == $editor->getId() ) {
286 wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
287 } elseif ( $wgBlockDisablesLogin && $targetUser->getBlock() ) {
288 // @TODO Partial blocks should not prevent the user from logging in.
289 // see: https://phabricator.wikimedia.org/T208895
290 wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
291 } elseif ( $targetUser->getOption( 'enotifusertalkpages' )
292 && ( !$minorEdit || $targetUser->getOption( 'enotifminoredits' ) )
293 ) {
294 if ( !$targetUser->isEmailConfirmed() ) {
295 wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
296 } elseif ( !Hooks::runner()->onAbortTalkPageEmailNotification( $targetUser, $title ) ) {
297 wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" );
298 } else {
299 wfDebug( __METHOD__ . ": sending talk page update notification" );
300 return true;
301 }
302 } else {
303 wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
304 }
305 }
306 return false;
307 }
308
313 private function composeCommonMailtext( MessageCache $messageCache ) {
317
318 $this->composed_common = true;
319
320 # You as the WikiAdmin and Sysops can make use of plenty of
321 # named variables when composing your notification emails while
322 # simply editing the Meta pages
323
324 $keys = [];
325 $postTransformKeys = [];
326 $pageTitleUrl = $this->title->getCanonicalURL();
327 $pageTitle = $this->title->getPrefixedText();
328
329 if ( $this->oldid ) {
330 // Always show a link to the diff which triggered the mail. See T34210.
331 $keys['$NEWPAGE'] = "\n\n" . wfMessage( 'enotif_lastdiff',
332 $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] ) )
333 ->inContentLanguage()->text();
334
335 if ( !$wgEnotifImpersonal ) {
336 // For personal mail, also show a link to the diff of all changes
337 // since last visited.
338 $keys['$NEWPAGE'] .= "\n\n" . wfMessage( 'enotif_lastvisited',
339 $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] ) )
340 ->inContentLanguage()->text();
341 }
342 $keys['$OLDID'] = $this->oldid;
343 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
344 $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
345 } else {
346 # clear $OLDID placeholder in the message template
347 $keys['$OLDID'] = '';
348 $keys['$NEWPAGE'] = '';
349 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
350 $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
351 }
352
353 $keys['$PAGETITLE'] = $this->title->getPrefixedText();
354 $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
355 $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
356 "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
357 '';
358 $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
359
360 if ( $this->editor->isAnon() ) {
361 # real anon (user:xxx.xxx.xxx.xxx)
362 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
363 ->inContentLanguage()->text();
364 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
365
366 } else {
367 $keys['$PAGEEDITOR'] = $wgEnotifUseRealName && $this->editor->getRealName() !== ''
368 ? $this->editor->getRealName() : $this->editor->getName();
369 $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
370 $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
371 }
372
373 $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
374 $keys['$HELPPAGE'] = wfExpandUrl(
375 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
376 );
377
378 # Replace this after transforming the message, T37019
379 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
380
381 // Now build message's subject and body
382
383 // Messages:
384 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
385 // enotif_subject_restored, enotif_subject_changed
386 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
387 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
388
389 // Messages:
390 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
391 // enotif_body_intro_restored, enotif_body_intro_changed
392 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
393 ->inContentLanguage()->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
394 ->text();
395
396 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
397 $body = strtr( $body, $keys );
398 $body = $messageCache->transform( $body, false, null, $this->title );
399 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
400
401 # Reveal the page editor's address as REPLY-TO address only if
402 # the user has not opted-out and the option is enabled at the
403 # global configuration level.
404 $adminAddress = new MailAddress( $wgPasswordSender,
405 wfMessage( 'emailsender' )->inContentLanguage()->text() );
407 && ( $this->editor->getEmail() != '' )
408 && $this->editor->getOption( 'enotifrevealaddr' )
409 ) {
410 $editorAddress = MailAddress::newFromUser( $this->editor );
411 if ( $wgEnotifFromEditor ) {
412 $this->from = $editorAddress;
413 } else {
414 $this->from = $adminAddress;
415 $this->replyto = $editorAddress;
416 }
417 } else {
418 $this->from = $adminAddress;
419 $this->replyto = new MailAddress( $wgNoReplyAddress );
420 }
421 }
422
432 private function compose( $user, $source, MessageCache $messageCache ) {
433 global $wgEnotifImpersonal;
434
435 if ( !$this->composed_common ) {
436 $this->composeCommonMailtext( $messageCache );
437 }
438
439 if ( $wgEnotifImpersonal ) {
440 $this->mailTargets[] = MailAddress::newFromUser( $user );
441 } else {
442 $this->sendPersonalised( $user, $source );
443 }
444 }
445
449 private function sendMails() {
450 global $wgEnotifImpersonal;
451 if ( $wgEnotifImpersonal ) {
452 $this->sendImpersonal( $this->mailTargets );
453 }
454 }
455
466 private function sendPersonalised( $watchingUser, $source ) {
468 // From the PHP manual:
469 // Note: The to parameter cannot be an address in the form of
470 // "Something <someone@example.com>". The mail command will not parse
471 // this properly while talking with the MTA.
472 $to = MailAddress::newFromUser( $watchingUser );
473
474 # $PAGEEDITDATE is the time and date of the page change
475 # expressed in terms of individual local time of the notification
476 # recipient, i.e. watching user
477 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
478 $body = str_replace(
479 [ '$WATCHINGUSERNAME',
480 '$PAGEEDITDATE',
481 '$PAGEEDITTIME' ],
482 [ $wgEnotifUseRealName && $watchingUser->getRealName() !== ''
483 ? $watchingUser->getRealName() : $watchingUser->getName(),
484 $contLang->userDate( $this->timestamp, $watchingUser ),
485 $contLang->userTime( $this->timestamp, $watchingUser ) ],
486 $this->body );
487
488 $headers = [];
489 if ( $source === self::WATCHLIST ) {
490 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
491 }
492
493 return UserMailer::send( $to, $this->from, $this->subject, $body, [
494 'replyTo' => $this->replyto,
495 'headers' => $headers,
496 ] );
497 }
498
505 private function sendImpersonal( $addresses ) {
506 if ( empty( $addresses ) ) {
507 return null;
508 }
509
510 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
511 $body = str_replace(
512 [ '$WATCHINGUSERNAME',
513 '$PAGEEDITDATE',
514 '$PAGEEDITTIME' ],
515 [ wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
516 $contLang->date( $this->timestamp, false, false ),
517 $contLang->time( $this->timestamp, false, false ) ],
518 $this->body );
519
520 return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
521 'replyTo' => $this->replyto,
522 ] );
523 }
524
525}
getPermissionManager()
bool $wgEnotifRevealEditorAddress
Allow sending of e-mail notifications with the editor's address in "Reply-To".
$wgEnotifWatchlist
Allow users to enable email notification ("enotif") on watchlist changes.
bool $wgEnotifFromEditor
Allow sending of e-mail notifications with the editor's address as sender.
$wgEnotifUserTalk
Allow users to enable email notification ("enotif") when someone edits their user talk page.
$wgNoReplyAddress
Reply-To address for e-mail notifications.
$wgEnotifImpersonal
Send a generic mail instead of a personalised mail for each user.
$wgUsersNotifiedOnAllChanges
Array of usernames who will be sent a notification email for every change which occurs on a wiki.
$wgBlockDisablesLogin
If true, blocked users will not be allowed to login.
$wgEnotifMinorEdits
Potentially send notification mails on minor edits to pages.
$wgPasswordSender
Sender email address for e-mail notifications.
$wgEnotifUseRealName
Use real name instead of username in e-mail "from" field.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
This module processes the email notifications when the current page is changed.
sendImpersonal( $addresses)
Same as sendPersonalised but does impersonal mail suitable for bulk mailing.
canSendUserTalkEmail( $editor, $title, $minorEdit)
const ALL_CHANGES
Notification because user is notified for all changes.
sendMails()
Send any queued mails.
notifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid=false, $pageStatus='changed')
Send emails corresponding to the user $editor editing the page $title.
sendPersonalised( $watchingUser, $source)
Does the per-user customizations to a notification e-mail (name, timestamp in proper timezone,...
const WATCHLIST
Notification is due to a watchlisted page being edited.
actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus='changed')
Immediate version of notifyOnPageChange().
const USER_TALK
Notification is due to user's user talk being edited.
composeCommonMailtext(MessageCache $messageCache)
Generate the generic "this page has been changed" e-mail text.
compose( $user, $source, MessageCache $messageCache)
Compose a mail to a given user and either queue it for sending, or send it now, depending on settings...
getPageStatus()
Extensions that have hooks for UpdateUserMailerFormattedPageStatus (to provide additional pageStatus ...
Job for email notification mails.
static singleton( $domain=false)
MediaWiki exception.
Stores a single person's name and email address.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
transform( $message, $interface=false, $language=null, $title=null)
static getSafeTitleFor( $name, $subpage=false)
Get a localised Title object for a page name with a possibly unvalidated subpage.
Represents a title within MediaWiki.
Definition Title.php:42
static newFromIDs( $ids)
Definition UserArray.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2150
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:541
getId()
Get the user's ID.
Definition User.php:2121
const NS_USER_TALK
Definition Defines.php:73
$source