MediaWiki  master
EmailNotification.php
Go to the documentation of this file.
1 <?php
32 
55 
59  private const USER_TALK = 'user_talk';
63  private const WATCHLIST = 'watchlist';
67  private const ALL_CHANGES = 'all_changes';
68 
70  protected $subject = '';
71 
73  protected $body = '';
74 
76  protected $replyto;
77 
79  protected $from;
80 
82  protected $timestamp;
83 
85  protected $summary = '';
86 
88  protected $minorEdit;
89 
91  protected $oldid;
92 
94  protected $composed_common = false;
95 
97  protected $pageStatus = '';
98 
100  protected $mailTargets = [];
101 
103  protected $title;
104 
106  protected $editor;
107 
118  public function getPageStatus() {
119  return $this->pageStatus;
120  }
121 
137  public function notifyOnPageChange(
139  $title,
140  $timestamp,
141  $summary,
142  $minorEdit,
143  $oldid = false,
144  $pageStatus = 'changed'
145  ): bool {
146  if ( $title->getNamespace() < 0 ) {
147  return false;
148  }
149 
150  $mwServices = MediaWikiServices::getInstance();
151  $config = $mwServices->getMainConfig();
152 
153  // update wl_notificationtimestamp for watchers
154  $watchers = [];
155  if ( $config->get( MainConfigNames::EnotifWatchlist ) ||
156  $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
157  $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
158  $editor->getUser(),
159  $title,
160  $timestamp
161  );
162  }
163 
164  $sendEmail = true;
165  // $watchers deals with $wgEnotifWatchlist.
166  // If nobody is watching the page, and there are no users notified on all changes
167  // don't bother creating a job/trying to send emails, unless it's a
168  // talk page with an applicable notification.
169  if ( $watchers === [] &&
170  !count( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) ) ) {
171  $sendEmail = false;
172  // Only send notification for non minor edits, unless $wgEnotifMinorEdits
173  if ( !$minorEdit ||
174  ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
175  !$editor->isAllowed( 'nominornewtalk' ) )
176  ) {
177  $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
178  if ( $config->get( MainConfigNames::EnotifUserTalk )
179  && $isUserTalkPage
180  && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
181  ) {
182  $sendEmail = true;
183  }
184  }
185  }
186 
187  if ( $sendEmail ) {
188  $mwServices->getJobQueueGroup()->lazyPush( new EnotifNotifyJob(
189  $title,
190  [
191  'editor' => $editor->getUser()->getName(),
192  'editorID' => $editor->getUser()->getId(),
193  'timestamp' => $timestamp,
194  'summary' => $summary,
195  'minorEdit' => $minorEdit,
196  'oldid' => $oldid,
197  'watchers' => $watchers,
198  'pageStatus' => $pageStatus
199  ]
200  ) );
201  }
202 
203  return $sendEmail;
204  }
205 
222  public function actuallyNotifyOnPageChange(
223  Authority $editor,
224  $title,
225  $timestamp,
226  $summary,
227  $minorEdit,
228  $oldid,
229  $watchers,
230  $pageStatus = 'changed'
231  ) {
232  # we use $wgPasswordSender as sender's address
233 
234  $mwServices = MediaWikiServices::getInstance();
235  $messageCache = $mwServices->getMessageCache();
236  $config = $mwServices->getMainConfig();
237 
238  # The following code is only run, if several conditions are met:
239  # 1. EmailNotification for pages (other than user_talk pages) must be enabled
240  # 2. minor edits (changes) are only regarded if the global flag indicates so
241 
242  $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
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  Hooks::runner()->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
256  if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
257  throw new MWException( 'Not a valid page status!' );
258  }
259 
260  $userTalkId = false;
261 
262  if ( !$minorEdit ||
263  ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
264  !$editor->isAllowed( 'nominornewtalk' ) )
265  ) {
266  if ( $config->get( MainConfigNames::EnotifUserTalk )
267  && $isUserTalkPage
268  && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
269  ) {
270  $targetUser = User::newFromName( $title->getText() );
271  $this->compose( $targetUser, self::USER_TALK, $messageCache );
272  $userTalkId = $targetUser->getId();
273  }
274 
275  if ( $config->get( MainConfigNames::EnotifWatchlist ) ) {
276  // Send updates to watchers other than the current editor
277  // and don't send to watchers who are blocked and cannot login
278  $userArray = UserArray::newFromIDs( $watchers );
279  foreach ( $userArray as $watchingUser ) {
280  if ( $watchingUser->getOption( 'enotifwatchlistpages' )
281  && ( !$minorEdit || $watchingUser->getOption( 'enotifminoredits' ) )
282  && $watchingUser->isEmailConfirmed()
283  && $watchingUser->getId() != $userTalkId
284  && !in_array( $watchingUser->getName(),
285  $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
286  // @TODO Partial blocks should not prevent the user from logging in.
287  // see: https://phabricator.wikimedia.org/T208895
288  && !( $config->get( MainConfigNames::BlockDisablesLogin ) &&
289  $watchingUser->getBlock() )
290  && Hooks::runner()->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
291  ) {
292  $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
293  }
294  }
295  }
296  }
297 
298  foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) {
299  if ( $editor->getUser()->getName() == $name ) {
300  // No point notifying the user that actually made the change!
301  continue;
302  }
303  $user = User::newFromName( $name );
304  $this->compose( $user, self::ALL_CHANGES, $messageCache );
305  }
306 
307  $this->sendMails();
308  }
309 
316  private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
317  $config = MediaWikiServices::getInstance()->getMainConfig();
318  $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
319 
320  if ( !$config->get( MainConfigNames::EnotifUserTalk ) || !$isUserTalkPage ) {
321  return false;
322  }
323 
324  $targetUser = User::newFromName( $title->getText() );
325 
326  if ( !$targetUser || $targetUser->isAnon() ) {
327  wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
328  } elseif ( $targetUser->getId() == $editor->getId() ) {
329  wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
330  } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
331  $targetUser->getBlock() ) {
332  // @TODO Partial blocks should not prevent the user from logging in.
333  // see: https://phabricator.wikimedia.org/T208895
334  wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
335  } elseif ( $targetUser->getOption( 'enotifusertalkpages' )
336  && ( !$minorEdit || $targetUser->getOption( 'enotifminoredits' ) )
337  ) {
338  if ( !$targetUser->isEmailConfirmed() ) {
339  wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
340  } elseif ( !Hooks::runner()->onAbortTalkPageEmailNotification( $targetUser, $title ) ) {
341  wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" );
342  } else {
343  wfDebug( __METHOD__ . ": sending talk page update notification" );
344  return true;
345  }
346  } else {
347  wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
348  }
349  return false;
350  }
351 
356  private function composeCommonMailtext( MessageCache $messageCache ) {
357  $config = MediaWikiServices::getInstance()->getMainConfig();
358 
359  $this->composed_common = true;
360 
361  # You as the WikiAdmin and Sysops can make use of plenty of
362  # named variables when composing your notification emails while
363  # simply editing the Meta pages
364 
365  $keys = [];
366  $postTransformKeys = [];
367  $pageTitleUrl = $this->title->getCanonicalURL();
368  $pageTitle = $this->title->getPrefixedText();
369 
370  if ( $this->oldid ) {
371  // Always show a link to the diff which triggered the mail. See T34210.
372  $keys['$NEWPAGE'] = "\n\n" . wfMessage(
373  'enotif_lastdiff',
374  $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
375  )->inContentLanguage()->text();
376 
377  if ( !$config->get( MainConfigNames::EnotifImpersonal ) ) {
378  // For personal mail, also show a link to the diff of all changes
379  // since last visited.
380  $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
381  'enotif_lastvisited',
382  $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
383  )->inContentLanguage()->text();
384  }
385  $keys['$OLDID'] = $this->oldid;
386  // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
387  $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
388  } else {
389  # clear $OLDID placeholder in the message template
390  $keys['$OLDID'] = '';
391  $keys['$NEWPAGE'] = '';
392  // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
393  $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
394  }
395 
396  $keys['$PAGETITLE'] = $this->title->getPrefixedText();
397  $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
398  $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
399  "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
400  '';
401  $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
402 
403  if ( !$this->editor->isRegistered() ) {
404  # real anon (user:xxx.xxx.xxx.xxx)
405  $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
406  ->inContentLanguage()->text();
407  $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
408 
409  } else {
410  $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
411  $this->editor->getRealName() !== ''
412  ? $this->editor->getRealName() : $this->editor->getName();
413  $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
414  $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
415  }
416 
417  $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
418  $keys['$HELPPAGE'] = wfExpandUrl(
419  Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
420  );
421 
422  # Replace this after transforming the message, T37019
423  $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
424 
425  // Now build message's subject and body
426 
427  // Messages:
428  // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
429  // enotif_subject_restored, enotif_subject_changed
430  $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
431  ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
432 
433  // Messages:
434  // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
435  // enotif_body_intro_restored, enotif_body_intro_changed
436  $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
437  ->inContentLanguage()
438  ->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
439  ->text();
440 
441  $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
442  $body = strtr( $body, $keys );
443  $body = $messageCache->transform( $body, false, null, $this->title );
444  $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
445 
446  # Reveal the page editor's address as REPLY-TO address only if
447  # the user has not opted-out and the option is enabled at the
448  # global configuration level.
449  $adminAddress = new MailAddress(
450  $config->get( MainConfigNames::PasswordSender ),
451  wfMessage( 'emailsender' )->inContentLanguage()->text()
452  );
453  if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
454  && ( $this->editor->getEmail() != '' )
455  && $this->editor->getOption( 'enotifrevealaddr' )
456  ) {
457  $editorAddress = MailAddress::newFromUser( $this->editor );
458  if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
459  $this->from = $editorAddress;
460  } else {
461  $this->from = $adminAddress;
462  $this->replyto = $editorAddress;
463  }
464  } else {
465  $this->from = $adminAddress;
466  $this->replyto = new MailAddress(
467  $config->get( MainConfigNames::NoReplyAddress )
468  );
469  }
470  }
471 
481  private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
482  if ( !$this->composed_common ) {
483  $this->composeCommonMailtext( $messageCache );
484  }
485 
486  if ( MediaWikiServices::getInstance()->getMainConfig()
487  ->get( MainConfigNames::EnotifImpersonal ) ) {
488  $this->mailTargets[] = MailAddress::newFromUser( $user );
489  } else {
490  $this->sendPersonalised( $user, $source );
491  }
492  }
493 
497  private function sendMails() {
498  if ( MediaWikiServices::getInstance()->getMainConfig()
499  ->get( MainConfigNames::EnotifImpersonal ) ) {
500  $this->sendImpersonal( $this->mailTargets );
501  }
502  }
503 
514  private function sendPersonalised( UserEmailContact $watchingUser, $source ) {
515  // From the PHP manual:
516  // Note: The to parameter cannot be an address in the form of
517  // "Something <someone@example.com>". The mail command will not parse
518  // this properly while talking with the MTA.
519  $to = MailAddress::newFromUser( $watchingUser );
520 
521  # $PAGEEDITDATE is the time and date of the page change
522  # expressed in terms of individual local time of the notification
523  # recipient, i.e. watching user
524  $mwServices = MediaWikiServices::getInstance();
525  $contLang = $mwServices->getContentLanguage();
526  $watchingUserName = (
527  $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
528  $watchingUser->getRealName() !== ''
529  ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
530  $body = str_replace(
531  [
532  '$WATCHINGUSERNAME',
533  '$PAGEEDITDATE',
534  '$PAGEEDITTIME'
535  ],
536  [
537  $watchingUserName,
538  $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
539  $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
540  ],
541  $this->body
542  );
543 
544  $headers = [];
545  if ( $source === self::WATCHLIST ) {
546  $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
547  }
548 
549  return UserMailer::send( $to, $this->from, $this->subject, $body, [
550  'replyTo' => $this->replyto,
551  'headers' => $headers,
552  ] );
553  }
554 
561  private function sendImpersonal( $addresses ) {
562  if ( empty( $addresses ) ) {
563  return null;
564  }
565 
566  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
567  $body = str_replace(
568  [
569  '$WATCHINGUSERNAME',
570  '$PAGEEDITDATE',
571  '$PAGEEDITTIME'
572  ],
573  [
574  wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
575  $contLang->date( $this->timestamp, false, false ),
576  $contLang->time( $this->timestamp, false, false )
577  ],
578  $this->body
579  );
580 
581  return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
582  'replyTo' => $this->replyto,
583  ] );
584  }
585 
586 }
const NS_USER_TALK
Definition: Defines.php:67
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.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
This module processes the email notifications when the current page is changed.
sendPersonalised(UserEmailContact $watchingUser, $source)
Does the per-user customizations to a notification e-mail (name, timestamp in proper timezone,...
sendImpersonal( $addresses)
Same as sendPersonalised but does impersonal mail suitable for bulk mailing.
MailAddress null $replyto
const ALL_CHANGES
Notification because user is notified for all changes.
sendMails()
Send any queued mails.
canSendUserTalkEmail(UserIdentity $editor, $title, $minorEdit)
actuallyNotifyOnPageChange(Authority $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus='changed')
Immediate version of notifyOnPageChange().
const WATCHLIST
Notification is due to a watchlisted page being edited.
int null bool $oldid
notifyOnPageChange(Authority $editor, $title, $timestamp, $summary, $minorEdit, $oldid=false, $pageStatus='changed')
Send emails corresponding to the user $editor editing the page $title.
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(UserEmailContact $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 ...
MailAddress null $from
MailAddress[] $mailTargets
Job for email notification mails.
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
MediaWiki exception.
Definition: MWException.php:29
Stores a single person's name and email address.
Definition: MailAddress.php:36
static newFromUser(UserEmailContact $user)
Create a new MailAddress object for the given user.
Definition: MailAddress.php:72
A class containing constants representing the names of configuration variables.
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, PageReference $page=null)
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1169
static getSafeTitleFor( $name, $subpage=false)
Get a localised Title object for a page name with a possibly unvalidated subpage.
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1066
static newFromIDs( $ids)
Definition: UserArray.php:52
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:120
static newFromName( $name, $validate='valid')
Definition: User.php:599
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition: User.php:2602
getUser()
Definition: User.php:3694
getRealName()
Get user real name or an empty string if unknown.
getUser()
Get the identity of the user this contact belongs to.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
$source