MediaWiki REL1_34
EmailNotification.php
Go to the documentation of this file.
1<?php
28
49
53 const USER_TALK = 'user_talk';
57 const WATCHLIST = 'watchlist';
61 const ALL_CHANGES = 'all_changes';
62
65 protected $mailTargets = [];
66
70 protected $title;
71
75 protected $editor;
76
87 public function getPageStatus() {
88 return $this->pageStatus;
89 }
90
105 $minorEdit, $oldid = false, $pageStatus = 'changed'
106 ) {
108
109 if ( $title->getNamespace() < 0 ) {
110 return;
111 }
112
113 // update wl_notificationtimestamp for watchers
114 $config = RequestContext::getMain()->getConfig();
115 $watchers = [];
116 if ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) {
117 $watchers = MediaWikiServices::getInstance()->getWatchedItemStore()->updateNotificationTimestamp(
118 $editor,
119 $title,
121 );
122 }
123
124 $sendEmail = true;
125 // $watchers deals with $wgEnotifWatchlist.
126 // If nobody is watching the page, and there are no users notified on all changes
127 // don't bother creating a job/trying to send emails, unless it's a
128 // talk page with an applicable notification.
129 if ( $watchers === [] && !count( $wgUsersNotifiedOnAllChanges ) ) {
130 $sendEmail = false;
131 // Only send notification for non minor edits, unless $wgEnotifMinorEdits
132 if ( !$minorEdit || ( $wgEnotifMinorEdits && !MediaWikiServices::getInstance()
134 ->userHasRight( $editor, 'nominornewtalk' ) )
135 ) {
136 $isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
138 && $isUserTalkPage
139 && $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
140 ) {
141 $sendEmail = true;
142 }
143 }
144 }
145
146 if ( $sendEmail ) {
147 JobQueueGroup::singleton()->lazyPush( new EnotifNotifyJob(
148 $title,
149 [
150 'editor' => $editor->getName(),
151 'editorID' => $editor->getId(),
152 'timestamp' => $timestamp,
153 'summary' => $summary,
154 'minorEdit' => $minorEdit,
155 'oldid' => $oldid,
156 'watchers' => $watchers,
157 'pageStatus' => $pageStatus
158 ]
159 ) );
160 }
161 }
162
180 $editor,
181 $title,
183 $summary,
185 $oldid,
186 $watchers,
187 $pageStatus = 'changed'
188 ) {
189 # we use $wgPasswordSender as sender's address
193
194 $messageCache = MediaWikiServices::getInstance()->getMessageCache();
195
196 # The following code is only run, if several conditions are met:
197 # 1. EmailNotification for pages (other than user_talk pages) must be enabled
198 # 2. minor edits (changes) are only regarded if the global flag indicates so
199
200 $isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
201
202 $this->title = $title;
203 $this->timestamp = $timestamp;
204 $this->summary = $summary;
205 $this->minorEdit = $minorEdit;
206 $this->oldid = $oldid;
207 $this->editor = $editor;
208 $this->composed_common = false;
209 $this->pageStatus = $pageStatus;
210
211 $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
212
213 Hooks::run( 'UpdateUserMailerFormattedPageStatus', [ &$formattedPageStatus ] );
214 if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
215 throw new MWException( 'Not a valid page status!' );
216 }
217
218 $userTalkId = false;
219
220 if ( !$minorEdit || ( $wgEnotifMinorEdits && !MediaWikiServices::getInstance()
222 ->userHasRight( $editor, 'nominornewtalk' ) )
223 ) {
225 && $isUserTalkPage
226 && $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
227 ) {
228 $targetUser = User::newFromName( $title->getText() );
229 $this->compose( $targetUser, self::USER_TALK, $messageCache );
230 $userTalkId = $targetUser->getId();
231 }
232
233 if ( $wgEnotifWatchlist ) {
234 // Send updates to watchers other than the current editor
235 // and don't send to watchers who are blocked and cannot login
236 $userArray = UserArray::newFromIDs( $watchers );
237 foreach ( $userArray as $watchingUser ) {
238 if ( $watchingUser->getOption( 'enotifwatchlistpages' )
239 && ( !$minorEdit || $watchingUser->getOption( 'enotifminoredits' ) )
240 && $watchingUser->isEmailConfirmed()
241 && $watchingUser->getId() != $userTalkId
242 && !in_array( $watchingUser->getName(), $wgUsersNotifiedOnAllChanges )
243 // @TODO Partial blocks should not prevent the user from logging in.
244 // see: https://phabricator.wikimedia.org/T208895
245 && !( $wgBlockDisablesLogin && $watchingUser->getBlock() )
246 && Hooks::run( 'SendWatchlistEmailNotification', [ $watchingUser, $title, $this ] )
247 ) {
248 $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
249 }
250 }
251 }
252 }
253
254 foreach ( $wgUsersNotifiedOnAllChanges as $name ) {
255 if ( $editor->getName() == $name ) {
256 // No point notifying the user that actually made the change!
257 continue;
258 }
259 $user = User::newFromName( $name );
260 $this->compose( $user, self::ALL_CHANGES, $messageCache );
261 }
262
263 $this->sendMails();
264 }
265
272 private function canSendUserTalkEmail( $editor, $title, $minorEdit ) {
274 $isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
275
276 if ( $wgEnotifUserTalk && $isUserTalkPage ) {
277 $targetUser = User::newFromName( $title->getText() );
278
279 if ( !$targetUser || $targetUser->isAnon() ) {
280 wfDebug( __METHOD__ . ": user talk page edited, but user does not exist\n" );
281 } elseif ( $targetUser->getId() == $editor->getId() ) {
282 wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent\n" );
283 } elseif ( $wgBlockDisablesLogin && $targetUser->getBlock() ) {
284 // @TODO Partial blocks should not prevent the user from logging in.
285 // see: https://phabricator.wikimedia.org/T208895
286 wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent\n" );
287 } elseif ( $targetUser->getOption( 'enotifusertalkpages' )
288 && ( !$minorEdit || $targetUser->getOption( 'enotifminoredits' ) )
289 ) {
290 if ( !$targetUser->isEmailConfirmed() ) {
291 wfDebug( __METHOD__ . ": talk page owner doesn't have validated email\n" );
292 } elseif ( !Hooks::run( 'AbortTalkPageEmailNotification', [ $targetUser, $title ] ) ) {
293 wfDebug( __METHOD__ . ": talk page update notification is aborted for this user\n" );
294 } else {
295 wfDebug( __METHOD__ . ": sending talk page update notification\n" );
296 return true;
297 }
298 } else {
299 wfDebug( __METHOD__ . ": talk page owner doesn't want notifications\n" );
300 }
301 }
302 return false;
303 }
304
309 private function composeCommonMailtext( MessageCache $messageCache ) {
313
314 $this->composed_common = true;
315
316 # You as the WikiAdmin and Sysops can make use of plenty of
317 # named variables when composing your notification emails while
318 # simply editing the Meta pages
319
320 $keys = [];
321 $postTransformKeys = [];
322 $pageTitleUrl = $this->title->getCanonicalURL();
323 $pageTitle = $this->title->getPrefixedText();
324
325 if ( $this->oldid ) {
326 // Always show a link to the diff which triggered the mail. See T34210.
327 $keys['$NEWPAGE'] = "\n\n" . wfMessage( 'enotif_lastdiff',
328 $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] ) )
329 ->inContentLanguage()->text();
330
331 if ( !$wgEnotifImpersonal ) {
332 // For personal mail, also show a link to the diff of all changes
333 // since last visited.
334 $keys['$NEWPAGE'] .= "\n\n" . wfMessage( 'enotif_lastvisited',
335 $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] ) )
336 ->inContentLanguage()->text();
337 }
338 $keys['$OLDID'] = $this->oldid;
339 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
340 $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
341 } else {
342 # clear $OLDID placeholder in the message template
343 $keys['$OLDID'] = '';
344 $keys['$NEWPAGE'] = '';
345 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
346 $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
347 }
348
349 $keys['$PAGETITLE'] = $this->title->getPrefixedText();
350 $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
351 $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
352 "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
353 '';
354 $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
355
356 if ( $this->editor->isAnon() ) {
357 # real anon (user:xxx.xxx.xxx.xxx)
358 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
359 ->inContentLanguage()->text();
360 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
361
362 } else {
363 $keys['$PAGEEDITOR'] = $wgEnotifUseRealName && $this->editor->getRealName() !== ''
364 ? $this->editor->getRealName() : $this->editor->getName();
365 $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
366 $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
367 }
368
369 $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
370 $keys['$HELPPAGE'] = wfExpandUrl(
371 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
372 );
373
374 # Replace this after transforming the message, T37019
375 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
376
377 // Now build message's subject and body
378
379 // Messages:
380 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
381 // enotif_subject_restored, enotif_subject_changed
382 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
383 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
384
385 // Messages:
386 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
387 // enotif_body_intro_restored, enotif_body_intro_changed
388 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
389 ->inContentLanguage()->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
390 ->text();
391
392 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
393 $body = strtr( $body, $keys );
394 $body = $messageCache->transform( $body, false, null, $this->title );
395 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
396
397 # Reveal the page editor's address as REPLY-TO address only if
398 # the user has not opted-out and the option is enabled at the
399 # global configuration level.
400 $adminAddress = new MailAddress( $wgPasswordSender,
401 wfMessage( 'emailsender' )->inContentLanguage()->text() );
403 && ( $this->editor->getEmail() != '' )
404 && $this->editor->getOption( 'enotifrevealaddr' )
405 ) {
406 $editorAddress = MailAddress::newFromUser( $this->editor );
407 if ( $wgEnotifFromEditor ) {
408 $this->from = $editorAddress;
409 } else {
410 $this->from = $adminAddress;
411 $this->replyto = $editorAddress;
412 }
413 } else {
414 $this->from = $adminAddress;
415 $this->replyto = new MailAddress( $wgNoReplyAddress );
416 }
417 }
418
428 private function compose( $user, $source, MessageCache $messageCache ) {
429 global $wgEnotifImpersonal;
430
431 if ( !$this->composed_common ) {
432 $this->composeCommonMailtext( $messageCache );
433 }
434
435 if ( $wgEnotifImpersonal ) {
436 $this->mailTargets[] = MailAddress::newFromUser( $user );
437 } else {
438 $this->sendPersonalised( $user, $source );
439 }
440 }
441
445 private function sendMails() {
446 global $wgEnotifImpersonal;
447 if ( $wgEnotifImpersonal ) {
448 $this->sendImpersonal( $this->mailTargets );
449 }
450 }
451
462 private function sendPersonalised( $watchingUser, $source ) {
464 // From the PHP manual:
465 // Note: The to parameter cannot be an address in the form of
466 // "Something <someone@example.com>". The mail command will not parse
467 // this properly while talking with the MTA.
468 $to = MailAddress::newFromUser( $watchingUser );
469
470 # $PAGEEDITDATE is the time and date of the page change
471 # expressed in terms of individual local time of the notification
472 # recipient, i.e. watching user
473 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
474 $body = str_replace(
475 [ '$WATCHINGUSERNAME',
476 '$PAGEEDITDATE',
477 '$PAGEEDITTIME' ],
478 [ $wgEnotifUseRealName && $watchingUser->getRealName() !== ''
479 ? $watchingUser->getRealName() : $watchingUser->getName(),
480 $contLang->userDate( $this->timestamp, $watchingUser ),
481 $contLang->userTime( $this->timestamp, $watchingUser ) ],
482 $this->body );
483
484 $headers = [];
485 if ( $source === self::WATCHLIST ) {
486 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
487 }
488
489 return UserMailer::send( $to, $this->from, $this->subject, $body, [
490 'replyTo' => $this->replyto,
491 'headers' => $headers,
492 ] );
493 }
494
501 private function sendImpersonal( $addresses ) {
502 if ( empty( $addresses ) ) {
503 return null;
504 }
505
506 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
507 $body = str_replace(
508 [ '$WATCHINGUSERNAME',
509 '$PAGEEDITDATE',
510 '$PAGEEDITTIME' ],
511 [ wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
512 $contLang->date( $this->timestamp, false, false ),
513 $contLang->time( $this->timestamp, false, false ) ],
514 $this->body );
515
516 return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
517 'replyTo' => $this->replyto,
518 ] );
519 }
520
521}
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.
MediaWiki exception.
Stores a single person's name and email address.
static newFromUser(User $user)
Create a new MailAddress object for the given user.
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)
Represents a title within MediaWiki.
Definition Title.php:42
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1037
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:995
static newFromIDs( $ids)
Definition UserArray.php:42
static send( $to, $from, $subject, $body, $options=[])
This function will perform a direct (authenticated) login to a SMTP Server to use for mail relaying i...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2364
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:518
getId()
Get the user's ID.
Definition User.php:2335
const NS_USER_TALK
Definition Defines.php:72
$source