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  $userOptionsLookup = $mwServices->getUserOptionsLookup();
277  // Send updates to watchers other than the current editor
278  // and don't send to watchers who are blocked and cannot login
279  $userArray = UserArray::newFromIDs( $watchers );
280  foreach ( $userArray as $watchingUser ) {
281  if ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' )
282  && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, 'enotifminoredits' ) )
283  && $watchingUser->isEmailConfirmed()
284  && $watchingUser->getId() != $userTalkId
285  && !in_array( $watchingUser->getName(),
286  $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
287  // @TODO Partial blocks should not prevent the user from logging in.
288  // see: https://phabricator.wikimedia.org/T208895
289  && !( $config->get( MainConfigNames::BlockDisablesLogin ) &&
290  $watchingUser->getBlock() )
291  && Hooks::runner()->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
292  ) {
293  $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
294  }
295  }
296  }
297  }
298 
299  foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) {
300  if ( $editor->getUser()->getName() == $name ) {
301  // No point notifying the user that actually made the change!
302  continue;
303  }
304  $user = User::newFromName( $name );
305  if ( $user instanceof User ) {
306  $this->compose( $user, self::ALL_CHANGES, $messageCache );
307  }
308  }
309  $this->sendMails();
310  }
311 
318  private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
319  $services = MediaWikiServices::getInstance();
320  $config = $services->getMainConfig();
321  $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
322 
323  if ( !$config->get( MainConfigNames::EnotifUserTalk ) || !$isUserTalkPage ) {
324  return false;
325  }
326 
327  $userOptionsLookup = $services->getUserOptionsLookup();
328  $targetUser = User::newFromName( $title->getText() );
329 
330  if ( !$targetUser || $targetUser->isAnon() ) {
331  wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
332  } elseif ( $targetUser->getId() == $editor->getId() ) {
333  wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
334  } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
335  $targetUser->getBlock() ) {
336  // @TODO Partial blocks should not prevent the user from logging in.
337  // see: https://phabricator.wikimedia.org/T208895
338  wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
339  } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' )
340  && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) )
341  ) {
342  if ( !$targetUser->isEmailConfirmed() ) {
343  wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
344  } elseif ( !Hooks::runner()->onAbortTalkPageEmailNotification( $targetUser, $title ) ) {
345  wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" );
346  } else {
347  wfDebug( __METHOD__ . ": sending talk page update notification" );
348  return true;
349  }
350  } else {
351  wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
352  }
353  return false;
354  }
355 
360  private function composeCommonMailtext( MessageCache $messageCache ) {
361  $services = MediaWikiServices::getInstance();
362  $config = $services->getMainConfig();
363  $userOptionsLookup = $services->getUserOptionsLookup();
364 
365  $this->composed_common = true;
366 
367  # You as the WikiAdmin and Sysops can make use of plenty of
368  # named variables when composing your notification emails while
369  # simply editing the Meta pages
370 
371  $keys = [];
372  $postTransformKeys = [];
373  $pageTitleUrl = $this->title->getCanonicalURL();
374  $pageTitle = $this->title->getPrefixedText();
375 
376  if ( $this->oldid ) {
377  // Always show a link to the diff which triggered the mail. See T34210.
378  $keys['$NEWPAGE'] = "\n\n" . wfMessage(
379  'enotif_lastdiff',
380  $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
381  )->inContentLanguage()->text();
382 
383  if ( !$config->get( MainConfigNames::EnotifImpersonal ) ) {
384  // For personal mail, also show a link to the diff of all changes
385  // since last visited.
386  $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
387  'enotif_lastvisited',
388  $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
389  )->inContentLanguage()->text();
390  }
391  $keys['$OLDID'] = $this->oldid;
392  // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
393  $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
394  } else {
395  # clear $OLDID placeholder in the message template
396  $keys['$OLDID'] = '';
397  $keys['$NEWPAGE'] = '';
398  // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
399  $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
400  }
401 
402  $keys['$PAGETITLE'] = $this->title->getPrefixedText();
403  $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
404  $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
405  "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
406  '';
407  $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
408 
409  if ( !$this->editor->isRegistered() ) {
410  # real anon (user:xxx.xxx.xxx.xxx)
411  $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
412  ->inContentLanguage()->text();
413  $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
414 
415  } else {
416  $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
417  $this->editor->getRealName() !== ''
418  ? $this->editor->getRealName() : $this->editor->getName();
419  $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
420  $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
421  }
422 
423  $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
424  $keys['$HELPPAGE'] = wfExpandUrl(
425  Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
426  );
427 
428  # Replace this after transforming the message, T37019
429  $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
430 
431  // Now build message's subject and body
432 
433  // Messages:
434  // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
435  // enotif_subject_restored, enotif_subject_changed
436  $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
437  ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
438 
439  // Messages:
440  // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
441  // enotif_body_intro_restored, enotif_body_intro_changed
442  $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
443  ->inContentLanguage()
444  ->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
445  ->text();
446 
447  $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
448  $body = strtr( $body, $keys );
449  $body = $messageCache->transform( $body, false, null, $this->title );
450  $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
451 
452  # Reveal the page editor's address as REPLY-TO address only if
453  # the user has not opted-out and the option is enabled at the
454  # global configuration level.
455  $adminAddress = new MailAddress(
456  $config->get( MainConfigNames::PasswordSender ),
457  wfMessage( 'emailsender' )->inContentLanguage()->text()
458  );
459  if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
460  && ( $this->editor->getEmail() != '' )
461  && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
462  ) {
463  $editorAddress = MailAddress::newFromUser( $this->editor );
464  if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
465  $this->from = $editorAddress;
466  } else {
467  $this->from = $adminAddress;
468  $this->replyto = $editorAddress;
469  }
470  } else {
471  $this->from = $adminAddress;
472  $this->replyto = new MailAddress(
473  $config->get( MainConfigNames::NoReplyAddress )
474  );
475  }
476  }
477 
487  private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
488  if ( !$this->composed_common ) {
489  $this->composeCommonMailtext( $messageCache );
490  }
491 
492  if ( MediaWikiServices::getInstance()->getMainConfig()
493  ->get( MainConfigNames::EnotifImpersonal ) ) {
494  $this->mailTargets[] = MailAddress::newFromUser( $user );
495  } else {
496  $this->sendPersonalised( $user, $source );
497  }
498  }
499 
503  private function sendMails() {
504  if ( MediaWikiServices::getInstance()->getMainConfig()
505  ->get( MainConfigNames::EnotifImpersonal ) ) {
506  $this->sendImpersonal( $this->mailTargets );
507  }
508  }
509 
520  private function sendPersonalised( UserEmailContact $watchingUser, $source ) {
521  // From the PHP manual:
522  // Note: The to parameter cannot be an address in the form of
523  // "Something <someone@example.com>". The mail command will not parse
524  // this properly while talking with the MTA.
525  $to = MailAddress::newFromUser( $watchingUser );
526 
527  # $PAGEEDITDATE is the time and date of the page change
528  # expressed in terms of individual local time of the notification
529  # recipient, i.e. watching user
530  $mwServices = MediaWikiServices::getInstance();
531  $contLang = $mwServices->getContentLanguage();
532  $watchingUserName = (
533  $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
534  $watchingUser->getRealName() !== ''
535  ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
536  $body = str_replace(
537  [
538  '$WATCHINGUSERNAME',
539  '$PAGEEDITDATE',
540  '$PAGEEDITTIME'
541  ],
542  [
543  $watchingUserName,
544  $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
545  $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
546  ],
547  $this->body
548  );
549 
550  $headers = [];
551  if ( $source === self::WATCHLIST ) {
552  $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
553  }
554 
555  return UserMailer::send( $to, $this->from, $this->subject, $body, [
556  'replyTo' => $this->replyto,
557  'headers' => $headers,
558  ] );
559  }
560 
567  private function sendImpersonal( $addresses ) {
568  if ( empty( $addresses ) ) {
569  return null;
570  }
571 
572  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
573  $body = str_replace(
574  [
575  '$WATCHINGUSERNAME',
576  '$PAGEEDITDATE',
577  '$PAGEEDITTIME'
578  ],
579  [
580  wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
581  $contLang->date( $this->timestamp, false, false ),
582  $contLang->time( $this->timestamp, false, false )
583  ],
584  $this->body
585  );
586 
587  return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
588  'replyTo' => $this->replyto,
589  ] );
590  }
591 
592 }
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.
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
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.
Service locator for MediaWiki core services.
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:1191
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
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
static newFromName( $name, $validate='valid')
Definition: User.php:598
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition: User.php:2364
getUser()
Definition: User.php:3459
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