MediaWiki  master
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;
65  protected $mailTargets = [];
66 
70  protected $title;
71 
75  protected $editor;
76 
87  public function getPageStatus() {
88  return $this->pageStatus;
89  }
90 
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 );
139  if ( $wgEnotifUserTalk
140  && $isUserTalkPage
141  && $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
142  ) {
143  $sendEmail = true;
144  }
145  }
146  }
147 
148  if ( $sendEmail ) {
149  JobQueueGroup::singleton()->lazyPush( new EnotifNotifyJob(
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 
183  public function actuallyNotifyOnPageChange(
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  ) {
228  if ( $wgEnotifUserTalk
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 ) {
467  global $wgEnotifUseRealName;
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 }
EmailNotification\$summary
$summary
Definition: EmailNotification.php:64
MessageCache\transform
transform( $message, $interface=false, $language=null, $title=null)
Definition: MessageCache.php:1223
User\getId
getId()
Get the user's ID.
Definition: User.php:2024
$wgEnotifMinorEdits
$wgEnotifMinorEdits
Potentially send notification mails on minor edits to pages.
Definition: DefaultSettings.php:1953
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:165
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:106
$wgUsersNotifiedOnAllChanges
$wgUsersNotifiedOnAllChanges
Array of usernames who will be sent a notification email for every change which occurs on a wiki.
Definition: DefaultSettings.php:1979
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:1895
EmailNotification\sendMails
sendMails()
Send any queued mails.
Definition: EmailNotification.php:449
$wgEnotifWatchlist
$wgEnotifWatchlist
Allow users to enable email notification ("enotif") on watchlist changes.
Definition: DefaultSettings.php:1913
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:545
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1220
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:70
SpecialPage\getSafeTitleFor
static getSafeTitleFor( $name, $subpage=false)
Get a localised Title object for a page name with a possibly unvalidated subpage.
Definition: SpecialPage.php:135
MailAddress
Stores a single person's name and email address.
Definition: MailAddress.php:34
$wgEnotifUseRealName
$wgEnotifUseRealName
Use real name instead of username in e-mail "from" field.
Definition: DefaultSettings.php:1973
$wgNoReplyAddress
$wgNoReplyAddress
Reply-To address for e-mail notifications.
Definition: DefaultSettings.php:1783
MWException
MediaWiki exception.
Definition: MWException.php:29
Title\getNamespace
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1034
getPermissionManager
getPermissionManager()
EmailNotification\composeCommonMailtext
composeCommonMailtext(MessageCache $messageCache)
Generate the generic "this page has been changed" e-mail text.
Definition: EmailNotification.php:313
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:910
$wgEnotifUserTalk
$wgEnotifUserTalk
Allow users to enable email notification ("enotif") when someone edits their user talk page.
Definition: DefaultSettings.php:1922
EmailNotification\$timestamp
$timestamp
Definition: EmailNotification.php:64
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:72
EmailNotification\actuallyNotifyOnPageChange
actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus='changed')
Immediate version of notifyOnPageChange().
Definition: EmailNotification.php:183
EmailNotification\sendPersonalised
sendPersonalised( $watchingUser, $source)
Does the per-user customizations to a notification e-mail (name, timestamp in proper timezone,...
Definition: EmailNotification.php:466
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
$wgEnotifRevealEditorAddress
bool $wgEnotifRevealEditorAddress
Allow sending of e-mail notifications with the editor's address in "Reply-To".
Definition: DefaultSettings.php:1938
$wgBlockDisablesLogin
$wgBlockDisablesLogin
If true, blocked users will not be allowed to login.
Definition: DefaultSettings.php:5464
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:432
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:454
Title
Represents a title within MediaWiki.
Definition: Title.php:41
EmailNotification\canSendUserTalkEmail
canSendUserTalkEmail( $editor, $title, $minorEdit)
Definition: EmailNotification.php:276
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:505
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:72
$source
$source
Definition: mwdoc-filter.php:34
$wgEnotifImpersonal
$wgEnotifImpersonal
Send a generic mail instead of a personalised mail for each user.
Definition: DefaultSettings.php:1962
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:1776
MessageCache
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
Definition: MessageCache.php:50
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:56
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
User\getName
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2053
EmailNotification\$pageStatus
$pageStatus
Definition: EmailNotification.php:64
Title\getText
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:1007
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:1310
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:490