MediaWiki  master
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 
63  protected $subject, $body, $replyto, $from;
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,
120  $timestamp
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 );
137  if ( $wgEnotifUserTalk
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 
179  public function actuallyNotifyOnPageChange(
180  $editor,
181  $title,
182  $timestamp,
183  $summary,
184  $minorEdit,
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  ) {
224  if ( $wgEnotifUserTalk
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 ) {
463  global $wgEnotifUseRealName;
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 }
EmailNotification\$summary
$summary
Definition: EmailNotification.php:64
MessageCache\transform
transform( $message, $interface=false, $language=null, $title=null)
Definition: MessageCache.php:1188
User\getId
getId()
Get the user's ID.
Definition: User.php:2253
$wgEnotifMinorEdits
$wgEnotifMinorEdits
Potentially send notification mails on minor edits to pages.
Definition: DefaultSettings.php:1850
EmailNotification\WATCHLIST
const WATCHLIST
Notification is due to a watchlisted page being edited.
Definition: EmailNotification.php:57
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:129
EmailNotification\notifyOnPageChange
notifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid=false, $pageStatus='changed')
Send emails corresponding to the user $editor editing the page $title.
Definition: EmailNotification.php:104
$wgUsersNotifiedOnAllChanges
$wgUsersNotifiedOnAllChanges
Array of usernames who will be sent a notification email for every change which occurs on a wiki.
Definition: DefaultSettings.php:1876
UserMailer\send
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...
Definition: UserMailer.php:115
$wgEnotifFromEditor
bool $wgEnotifFromEditor
Allow sending of e-mail notifications with the editor's address as sender.
Definition: DefaultSettings.php:1792
EmailNotification\sendMails
sendMails()
Send any queued mails.
Definition: EmailNotification.php:445
$wgEnotifWatchlist
$wgEnotifWatchlist
Allow users to enable email notification ("enotif") on watchlist changes.
Definition: DefaultSettings.php:1810
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1263
EmailNotification\$body
$body
Definition: EmailNotification.php:63
EmailNotification\$replyto
$replyto
Definition: EmailNotification.php:63
MailAddress\newFromUser
static newFromUser(User $user)
Create a new MailAddress object for the given user.
Definition: MailAddress.php:66
SpecialPage\getSafeTitleFor
static getSafeTitleFor( $name, $subpage=false)
Get a localised Title object for a page name with a possibly unvalidated subpage.
Definition: SpecialPage.php:112
MailAddress
Stores a single person's name and email address.
Definition: MailAddress.php:32
$wgEnotifUseRealName
$wgEnotifUseRealName
Use real name instead of username in e-mail "from" field.
Definition: DefaultSettings.php:1870
$wgNoReplyAddress
$wgNoReplyAddress
Reply-To address for e-mail notifications.
Definition: DefaultSettings.php:1680
MWException
MediaWiki exception.
Definition: MWException.php:26
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1035
getPermissionManager
getPermissionManager()
EmailNotification\composeCommonMailtext
composeCommonMailtext(MessageCache $messageCache)
Generate the generic "this page has been changed" e-mail text.
Definition: EmailNotification.php:309
EmailNotification\$oldid
$oldid
Definition: EmailNotification.php:64
EmailNotification\$minorEdit
$minorEdit
Definition: EmailNotification.php:64
UserArray\newFromIDs
static newFromIDs( $ids)
Definition: UserArray.php:42
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
$wgEnotifUserTalk
$wgEnotifUserTalk
Allow users to enable email notification ("enotif") when someone edits their user talk page.
Definition: DefaultSettings.php:1819
EmailNotification\$timestamp
$timestamp
Definition: EmailNotification.php:64
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:63
EmailNotification\actuallyNotifyOnPageChange
actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus='changed')
Immediate version of notifyOnPageChange().
Definition: EmailNotification.php:179
EmailNotification\sendPersonalised
sendPersonalised( $watchingUser, $source)
Does the per-user customizations to a notification e-mail (name, timestamp in proper timezone,...
Definition: EmailNotification.php:462
$wgEnotifRevealEditorAddress
bool $wgEnotifRevealEditorAddress
Allow sending of e-mail notifications with the editor's address in "Reply-To".
Definition: DefaultSettings.php:1835
$wgBlockDisablesLogin
$wgBlockDisablesLogin
If true, blocked users will not be allowed to login.
Definition: DefaultSettings.php:5025
EmailNotification\compose
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...
Definition: EmailNotification.php:428
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:445
Title
Represents a title within MediaWiki.
Definition: Title.php:42
EmailNotification\canSendUserTalkEmail
canSendUserTalkEmail( $editor, $title, $minorEdit)
Definition: EmailNotification.php:272
JobQueueGroup\singleton
static singleton( $domain=false)
Definition: JobQueueGroup.php:70
EmailNotification\USER_TALK
const USER_TALK
Notification is due to user's user talk being edited.
Definition: EmailNotification.php:53
EmailNotification\$mailTargets
$mailTargets
Definition: EmailNotification.php:65
EmailNotification\sendImpersonal
sendImpersonal( $addresses)
Same as sendPersonalised but does impersonal mail suitable for bulk mailing.
Definition: EmailNotification.php:501
EmailNotification\getPageStatus
getPageStatus()
Extensions that have hooks for UpdateUserMailerFormattedPageStatus (to provide additional pageStatus ...
Definition: EmailNotification.php:87
EmailNotification\$title
Title $title
Definition: EmailNotification.php:70
$keys
$keys
Definition: testCompression.php:69
$source
$source
Definition: mwdoc-filter.php:34
$wgEnotifImpersonal
$wgEnotifImpersonal
Send a generic mail instead of a personalised mail for each user.
Definition: DefaultSettings.php:1859
EmailNotification\$editor
User $editor
Definition: EmailNotification.php:75
EmailNotification
This module processes the email notifications when the current page is changed.
Definition: EmailNotification.php:48
EmailNotification\$subject
$subject
Definition: EmailNotification.php:63
$wgPasswordSender
$wgPasswordSender
Sender email address for e-mail notifications.
Definition: DefaultSettings.php:1673
MessageCache
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
Definition: MessageCache.php:42
EmailNotification\$composed_common
$composed_common
Definition: EmailNotification.php:64
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
EmailNotification\ALL_CHANGES
const ALL_CHANGES
Notification because user is notified for all changes.
Definition: EmailNotification.php:61
EnotifNotifyJob
Job for email notification mails.
Definition: EnotifNotifyJob.php:29
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2282
EmailNotification\$pageStatus
$pageStatus
Definition: EmailNotification.php:64
Title\getText
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:996
EmailNotification\$from
$from
Definition: EmailNotification.php:63
Skin\makeInternalOrExternalUrl
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1224
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:491