MediaWiki  master
EmailNotification.php
Go to the documentation of this file.
1 <?php
38 
61 
65  private const USER_TALK = 'user_talk';
69  private const WATCHLIST = 'watchlist';
73  private const ALL_CHANGES = 'all_changes';
74 
76  protected $subject = '';
77 
79  protected $body = '';
80 
82  protected $replyto;
83 
85  protected $from;
86 
88  protected $timestamp;
89 
91  protected $summary = '';
92 
94  protected $minorEdit;
95 
97  protected $oldid;
98 
100  protected $composed_common = false;
101 
103  protected $pageStatus = '';
104 
106  protected $mailTargets = [];
107 
109  protected $title;
110 
112  protected $editor;
113 
124  public function getPageStatus() {
125  return $this->pageStatus;
126  }
127 
143  public function notifyOnPageChange(
145  $title,
146  $timestamp,
147  $summary,
148  $minorEdit,
149  $oldid = false,
150  $pageStatus = 'changed'
151  ): bool {
152  if ( $title->getNamespace() < 0 ) {
153  return false;
154  }
155 
156  $mwServices = MediaWikiServices::getInstance();
157  $config = $mwServices->getMainConfig();
158 
159  // update wl_notificationtimestamp for watchers
160  $watchers = [];
161  if ( $config->get( MainConfigNames::EnotifWatchlist ) ||
162  $config->get( MainConfigNames::ShowUpdatedMarker )
163  ) {
164  $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
165  $editor->getUser(),
166  $title,
167  $timestamp
168  );
169  }
170 
171  $sendEmail = true;
172  // $watchers deals with $wgEnotifWatchlist.
173  // If nobody is watching the page, and there are no users notified on all changes
174  // don't bother creating a job/trying to send emails, unless it's a
175  // talk page with an applicable notification.
176  if ( $watchers === [] &&
177  !count( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
178  ) {
179  $sendEmail = false;
180  // Only send notification for non minor edits, unless $wgEnotifMinorEdits
181  if ( !$minorEdit ||
182  ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
183  !$editor->isAllowed( 'nominornewtalk' ) )
184  ) {
185  if ( $config->get( MainConfigNames::EnotifUserTalk )
187  && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
188  ) {
189  $sendEmail = true;
190  }
191  }
192  }
193 
194  if ( $sendEmail ) {
195  $mwServices->getJobQueueGroup()->lazyPush( new EnotifNotifyJob(
196  $title,
197  [
198  'editor' => $editor->getUser()->getName(),
199  'editorID' => $editor->getUser()->getId(),
200  'timestamp' => $timestamp,
201  'summary' => $summary,
202  'minorEdit' => $minorEdit,
203  'oldid' => $oldid,
204  'watchers' => $watchers,
205  'pageStatus' => $pageStatus
206  ]
207  ) );
208  }
209 
210  return $sendEmail;
211  }
212 
228  public function actuallyNotifyOnPageChange(
229  Authority $editor,
230  $title,
231  $timestamp,
232  $summary,
233  $minorEdit,
234  $oldid,
235  $watchers,
236  $pageStatus = 'changed'
237  ) {
238  # we use $wgPasswordSender as sender's address
239 
240  $mwServices = MediaWikiServices::getInstance();
241  $messageCache = $mwServices->getMessageCache();
242  $config = $mwServices->getMainConfig();
243 
244  # The following code is only run, if several conditions are met:
245  # 1. EmailNotification for pages (other than user_talk pages) must be enabled
246  # 2. minor edits (changes) are only regarded if the global flag indicates so
247 
248  $this->title = $title;
249  $this->timestamp = $timestamp;
250  $this->summary = $summary;
251  $this->minorEdit = $minorEdit;
252  $this->oldid = $oldid;
253  $this->editor = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $editor );
254  $this->composed_common = false;
255  $this->pageStatus = $pageStatus;
256 
257  $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
258 
259  $hookRunner = new HookRunner( $mwServices->getHookContainer() );
260  $hookRunner->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
261  if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
262  throw new UnexpectedValueException( 'Not a valid page status!' );
263  }
264 
265  $userTalkId = false;
266 
267  if ( !$minorEdit ||
268  ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
269  !$editor->isAllowed( 'nominornewtalk' ) )
270  ) {
271  if ( $config->get( MainConfigNames::EnotifUserTalk )
272  && $title->getNamespace() === NS_USER_TALK
273  && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
274  ) {
275  $targetUser = User::newFromName( $title->getText() );
276  $this->compose( $targetUser, self::USER_TALK, $messageCache );
277  $userTalkId = $targetUser->getId();
278  }
279 
280  if ( $config->get( MainConfigNames::EnotifWatchlist ) ) {
281  $userOptionsLookup = $mwServices->getUserOptionsLookup();
282  // Send updates to watchers other than the current editor
283  // and don't send to watchers who are blocked and cannot login
284  $userArray = UserArray::newFromIDs( $watchers );
285  foreach ( $userArray as $watchingUser ) {
286  if ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' )
287  && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, 'enotifminoredits' ) )
288  && $watchingUser->isEmailConfirmed()
289  && $watchingUser->getId() != $userTalkId
290  && !in_array( $watchingUser->getName(),
291  $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
292  // @TODO Partial blocks should not prevent the user from logging in.
293  // see: https://phabricator.wikimedia.org/T208895
294  && !( $config->get( MainConfigNames::BlockDisablesLogin ) &&
295  $watchingUser->getBlock() )
296  && $hookRunner->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
297  ) {
298  $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
299  }
300  }
301  }
302  }
303 
304  foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) {
305  if ( $editor->getUser()->getName() == $name ) {
306  // No point notifying the user that actually made the change!
307  continue;
308  }
309  $user = User::newFromName( $name );
310  if ( $user instanceof User ) {
311  $this->compose( $user, self::ALL_CHANGES, $messageCache );
312  }
313  }
314  $this->sendMails();
315  }
316 
323  private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
324  $services = MediaWikiServices::getInstance();
325  $config = $services->getMainConfig();
326 
327  if ( !$config->get( MainConfigNames::EnotifUserTalk ) || $title->getNamespace() !== NS_USER_TALK ) {
328  return false;
329  }
330 
331  $userOptionsLookup = $services->getUserOptionsLookup();
332  $targetUser = User::newFromName( $title->getText() );
333 
334  if ( !$targetUser || $targetUser->isAnon() ) {
335  wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
336  } elseif ( $targetUser->getId() == $editor->getId() ) {
337  wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
338  } elseif ( $targetUser->isTemp() ) {
339  wfDebug( __METHOD__ . ": talk page owner is a temporary user so doesn't have email" );
340  } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
341  $targetUser->getBlock()
342  ) {
343  // @TODO Partial blocks should not prevent the user from logging in.
344  // see: https://phabricator.wikimedia.org/T208895
345  wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
346  } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' )
347  && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) )
348  ) {
349  if ( !$targetUser->isEmailConfirmed() ) {
350  wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
351  } elseif ( !( new HookRunner( $services->getHookContainer() ) )
352  ->onAbortTalkPageEmailNotification( $targetUser, $title )
353  ) {
354  wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" );
355  } else {
356  wfDebug( __METHOD__ . ": sending talk page update notification" );
357  return true;
358  }
359  } else {
360  wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
361  }
362  return false;
363  }
364 
369  private function composeCommonMailtext( MessageCache $messageCache ) {
370  $services = MediaWikiServices::getInstance();
371  $config = $services->getMainConfig();
372  $userOptionsLookup = $services->getUserOptionsLookup();
373 
374  $this->composed_common = true;
375 
376  # You as the WikiAdmin and Sysops can make use of plenty of
377  # named variables when composing your notification emails while
378  # simply editing the Meta pages
379 
380  $keys = [];
381  $postTransformKeys = [];
382  $pageTitleUrl = $this->title->getCanonicalURL();
383  $pageTitle = $this->title->getPrefixedText();
384 
385  if ( $this->oldid ) {
386  // Always show a link to the diff which triggered the mail. See T34210.
387  $keys['$NEWPAGE'] = "\n\n" . wfMessage(
388  'enotif_lastdiff',
389  $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
390  )->inContentLanguage()->text();
391 
392  if ( !$config->get( MainConfigNames::EnotifImpersonal ) ) {
393  // For personal mail, also show a link to the diff of all changes
394  // since last visited.
395  $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
396  'enotif_lastvisited',
397  $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
398  )->inContentLanguage()->text();
399  }
400  $keys['$OLDID'] = $this->oldid;
401  } else {
402  # clear $OLDID placeholder in the message template
403  $keys['$OLDID'] = '';
404  $keys['$NEWPAGE'] = '';
405  }
406 
407  $keys['$PAGETITLE'] = $this->title->getPrefixedText();
408  $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
409  $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
410  "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
411  '';
412  $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
413 
414  if ( $this->editor->isAnon() ) {
415  # real anon (user:xxx.xxx.xxx.xxx)
416  $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
417  ->inContentLanguage()->text();
418  $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
419  } elseif ( $this->editor->isTemp() ) {
420  $keys['$PAGEEDITOR'] = wfMessage( 'enotif_temp_editor', $this->editor->getName() )
421  ->inContentLanguage()->text();
422  $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
423  } else {
424  $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
425  $this->editor->getRealName() !== ''
426  ? $this->editor->getRealName() : $this->editor->getName();
427  $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
428  $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
429  }
430 
431  $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
432  $keys['$HELPPAGE'] = wfExpandUrl(
433  Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
434  );
435 
436  # Replace this after transforming the message, T37019
437  $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
438 
439  // Now build message's subject and body
440 
441  // Messages:
442  // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
443  // enotif_subject_restored, enotif_subject_changed
444  $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
445  ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
446 
447  // Messages:
448  // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
449  // enotif_body_intro_restored, enotif_body_intro_changed
450  $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
451  ->inContentLanguage()
452  ->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
453  ->text();
454 
455  $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
456  $body = strtr( $body, $keys );
457  $body = $messageCache->transform( $body, false, null, $this->title );
458  $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
459 
460  # Reveal the page editor's address as REPLY-TO address only if
461  # the user has not opted-out and the option is enabled at the
462  # global configuration level.
463  $adminAddress = new MailAddress(
464  $config->get( MainConfigNames::PasswordSender ),
465  wfMessage( 'emailsender' )->inContentLanguage()->text()
466  );
467  if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
468  && ( $this->editor->getEmail() != '' )
469  && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
470  ) {
471  $editorAddress = MailAddress::newFromUser( $this->editor );
472  if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
473  $this->from = $editorAddress;
474  } else {
475  $this->from = $adminAddress;
476  $this->replyto = $editorAddress;
477  }
478  } else {
479  $this->from = $adminAddress;
480  $this->replyto = new MailAddress(
481  $config->get( MainConfigNames::NoReplyAddress )
482  );
483  }
484  }
485 
495  private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
496  if ( !$this->composed_common ) {
497  $this->composeCommonMailtext( $messageCache );
498  }
499 
500  if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
501  $this->mailTargets[] = MailAddress::newFromUser( $user );
502  } else {
503  $this->sendPersonalised( $user, $source );
504  }
505  }
506 
510  private function sendMails() {
511  if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
512  $this->sendImpersonal( $this->mailTargets );
513  }
514  }
515 
526  private function sendPersonalised( UserEmailContact $watchingUser, $source ) {
527  // From the PHP manual:
528  // Note: The to parameter cannot be an address in the form of
529  // "Something <someone@example.com>". The mail command will not parse
530  // this properly while talking with the MTA.
531  $to = MailAddress::newFromUser( $watchingUser );
532 
533  # $PAGEEDITDATE is the time and date of the page change
534  # expressed in terms of individual local time of the notification
535  # recipient, i.e. watching user
536  $mwServices = MediaWikiServices::getInstance();
537  $contLang = $mwServices->getContentLanguage();
538  $watchingUserName = (
539  $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
540  $watchingUser->getRealName() !== ''
541  ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
542  $body = str_replace(
543  [
544  '$WATCHINGUSERNAME',
545  '$PAGEEDITDATE',
546  '$PAGEEDITTIME'
547  ],
548  [
549  $watchingUserName,
550  $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
551  $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
552  ],
553  $this->body
554  );
555 
556  $headers = [];
557  if ( $source === self::WATCHLIST ) {
558  $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
559  }
560 
561  return UserMailer::send( $to, $this->from, $this->subject, $body, [
562  'replyTo' => $this->replyto,
563  'headers' => $headers,
564  ] );
565  }
566 
573  private function sendImpersonal( $addresses ) {
574  if ( !$addresses ) {
575  return null;
576  }
577 
578  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
579  $body = str_replace(
580  [
581  '$WATCHINGUSERNAME',
582  '$PAGEEDITDATE',
583  '$PAGEEDITTIME'
584  ],
585  [
586  wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
587  $contLang->date( $this->timestamp, false, false ),
588  $contLang->time( $this->timestamp, false, false )
589  ],
590  $this->body
591  );
592 
593  return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
594  'replyTo' => $this->replyto,
595  ] );
596  }
597 
598 }
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 using $wgServer (or one of its alternatives).
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
This module processes the email notifications when the current page is changed.
MailAddress null $replyto
actuallyNotifyOnPageChange(Authority $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus='changed')
Immediate version of notifyOnPageChange().
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.
getPageStatus()
Extensions that have hooks for UpdateUserMailerFormattedPageStatus (to provide additional pageStatus ...
MailAddress null $from
MailAddress[] $mailTargets
Send an email notification.
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
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:568
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Parent class for all special pages.
Definition: SpecialPage.php:66
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
Represents a title within MediaWiki.
Definition: Title.php:76
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1058
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:1031
internal since 1.36
Definition: User.php:98
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition: User.php:2363
Cache 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:1167
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:99
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, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
$source