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  ) {
158  $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
159  $editor->getUser(),
160  $title,
161  $timestamp
162  );
163  }
164 
165  $sendEmail = true;
166  // $watchers deals with $wgEnotifWatchlist.
167  // If nobody is watching the page, and there are no users notified on all changes
168  // don't bother creating a job/trying to send emails, unless it's a
169  // talk page with an applicable notification.
170  if ( $watchers === [] &&
171  !count( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
172  ) {
173  $sendEmail = false;
174  // Only send notification for non minor edits, unless $wgEnotifMinorEdits
175  if ( !$minorEdit ||
176  ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
177  !$editor->isAllowed( 'nominornewtalk' ) )
178  ) {
179  if ( $config->get( MainConfigNames::EnotifUserTalk )
181  && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
182  ) {
183  $sendEmail = true;
184  }
185  }
186  }
187 
188  if ( $sendEmail ) {
189  $mwServices->getJobQueueGroup()->lazyPush( new EnotifNotifyJob(
190  $title,
191  [
192  'editor' => $editor->getUser()->getName(),
193  'editorID' => $editor->getUser()->getId(),
194  'timestamp' => $timestamp,
195  'summary' => $summary,
196  'minorEdit' => $minorEdit,
197  'oldid' => $oldid,
198  'watchers' => $watchers,
199  'pageStatus' => $pageStatus
200  ]
201  ) );
202  }
203 
204  return $sendEmail;
205  }
206 
223  public function actuallyNotifyOnPageChange(
224  Authority $editor,
225  $title,
226  $timestamp,
227  $summary,
228  $minorEdit,
229  $oldid,
230  $watchers,
231  $pageStatus = 'changed'
232  ) {
233  # we use $wgPasswordSender as sender's address
234 
235  $mwServices = MediaWikiServices::getInstance();
236  $messageCache = $mwServices->getMessageCache();
237  $config = $mwServices->getMainConfig();
238 
239  # The following code is only run, if several conditions are met:
240  # 1. EmailNotification for pages (other than user_talk pages) must be enabled
241  # 2. minor edits (changes) are only regarded if the global flag indicates so
242 
243  $this->title = $title;
244  $this->timestamp = $timestamp;
245  $this->summary = $summary;
246  $this->minorEdit = $minorEdit;
247  $this->oldid = $oldid;
248  $this->editor = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $editor );
249  $this->composed_common = false;
250  $this->pageStatus = $pageStatus;
251 
252  $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
253 
254  Hooks::runner()->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
255  if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
256  throw new MWException( 'Not a valid page status!' );
257  }
258 
259  $userTalkId = false;
260 
261  if ( !$minorEdit ||
262  ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
263  !$editor->isAllowed( 'nominornewtalk' ) )
264  ) {
265  if ( $config->get( MainConfigNames::EnotifUserTalk )
266  && $title->getNamespace() === NS_USER_TALK
267  && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
268  ) {
269  $targetUser = User::newFromName( $title->getText() );
270  $this->compose( $targetUser, self::USER_TALK, $messageCache );
271  $userTalkId = $targetUser->getId();
272  }
273 
274  if ( $config->get( MainConfigNames::EnotifWatchlist ) ) {
275  $userOptionsLookup = $mwServices->getUserOptionsLookup();
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 ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' )
281  && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, '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  if ( $user instanceof User ) {
305  $this->compose( $user, self::ALL_CHANGES, $messageCache );
306  }
307  }
308  $this->sendMails();
309  }
310 
317  private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
318  $services = MediaWikiServices::getInstance();
319  $config = $services->getMainConfig();
320 
321  if ( !$config->get( MainConfigNames::EnotifUserTalk ) || $title->getNamespace() !== NS_USER_TALK ) {
322  return false;
323  }
324 
325  $userOptionsLookup = $services->getUserOptionsLookup();
326  $targetUser = User::newFromName( $title->getText() );
327 
328  if ( !$targetUser || $targetUser->isAnon() ) {
329  wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
330  } elseif ( $targetUser->getId() == $editor->getId() ) {
331  wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
332  } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
333  $targetUser->getBlock()
334  ) {
335  // @TODO Partial blocks should not prevent the user from logging in.
336  // see: https://phabricator.wikimedia.org/T208895
337  wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
338  } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' )
339  && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) )
340  ) {
341  if ( !$targetUser->isEmailConfirmed() ) {
342  wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
343  } elseif ( !Hooks::runner()->onAbortTalkPageEmailNotification( $targetUser, $title ) ) {
344  wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" );
345  } else {
346  wfDebug( __METHOD__ . ": sending talk page update notification" );
347  return true;
348  }
349  } else {
350  wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
351  }
352  return false;
353  }
354 
359  private function composeCommonMailtext( MessageCache $messageCache ) {
360  $services = MediaWikiServices::getInstance();
361  $config = $services->getMainConfig();
362  $userOptionsLookup = $services->getUserOptionsLookup();
363 
364  $this->composed_common = true;
365 
366  # You as the WikiAdmin and Sysops can make use of plenty of
367  # named variables when composing your notification emails while
368  # simply editing the Meta pages
369 
370  $keys = [];
371  $postTransformKeys = [];
372  $pageTitleUrl = $this->title->getCanonicalURL();
373  $pageTitle = $this->title->getPrefixedText();
374 
375  if ( $this->oldid ) {
376  // Always show a link to the diff which triggered the mail. See T34210.
377  $keys['$NEWPAGE'] = "\n\n" . wfMessage(
378  'enotif_lastdiff',
379  $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
380  )->inContentLanguage()->text();
381 
382  if ( !$config->get( MainConfigNames::EnotifImpersonal ) ) {
383  // For personal mail, also show a link to the diff of all changes
384  // since last visited.
385  $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
386  'enotif_lastvisited',
387  $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
388  )->inContentLanguage()->text();
389  }
390  $keys['$OLDID'] = $this->oldid;
391  // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
392  $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
393  } else {
394  # clear $OLDID placeholder in the message template
395  $keys['$OLDID'] = '';
396  $keys['$NEWPAGE'] = '';
397  // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
398  $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
399  }
400 
401  $keys['$PAGETITLE'] = $this->title->getPrefixedText();
402  $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
403  $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
404  "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
405  '';
406  $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
407 
408  if ( !$this->editor->isRegistered() ) {
409  # real anon (user:xxx.xxx.xxx.xxx)
410  $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
411  ->inContentLanguage()->text();
412  $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
413 
414  } else {
415  $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
416  $this->editor->getRealName() !== ''
417  ? $this->editor->getRealName() : $this->editor->getName();
418  $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
419  $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
420  }
421 
422  $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
423  $keys['$HELPPAGE'] = wfExpandUrl(
424  Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
425  );
426 
427  # Replace this after transforming the message, T37019
428  $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
429 
430  // Now build message's subject and body
431 
432  // Messages:
433  // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
434  // enotif_subject_restored, enotif_subject_changed
435  $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
436  ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
437 
438  // Messages:
439  // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
440  // enotif_body_intro_restored, enotif_body_intro_changed
441  $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
442  ->inContentLanguage()
443  ->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
444  ->text();
445 
446  $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
447  $body = strtr( $body, $keys );
448  $body = $messageCache->transform( $body, false, null, $this->title );
449  $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
450 
451  # Reveal the page editor's address as REPLY-TO address only if
452  # the user has not opted-out and the option is enabled at the
453  # global configuration level.
454  $adminAddress = new MailAddress(
455  $config->get( MainConfigNames::PasswordSender ),
456  wfMessage( 'emailsender' )->inContentLanguage()->text()
457  );
458  if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
459  && ( $this->editor->getEmail() != '' )
460  && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
461  ) {
462  $editorAddress = MailAddress::newFromUser( $this->editor );
463  if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
464  $this->from = $editorAddress;
465  } else {
466  $this->from = $adminAddress;
467  $this->replyto = $editorAddress;
468  }
469  } else {
470  $this->from = $adminAddress;
471  $this->replyto = new MailAddress(
472  $config->get( MainConfigNames::NoReplyAddress )
473  );
474  }
475  }
476 
486  private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
487  if ( !$this->composed_common ) {
488  $this->composeCommonMailtext( $messageCache );
489  }
490 
491  if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
492  $this->mailTargets[] = MailAddress::newFromUser( $user );
493  } else {
494  $this->sendPersonalised( $user, $source );
495  }
496  }
497 
501  private function sendMails() {
502  if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
503  $this->sendImpersonal( $this->mailTargets );
504  }
505  }
506 
517  private function sendPersonalised( UserEmailContact $watchingUser, $source ) {
518  // From the PHP manual:
519  // Note: The to parameter cannot be an address in the form of
520  // "Something <someone@example.com>". The mail command will not parse
521  // this properly while talking with the MTA.
522  $to = MailAddress::newFromUser( $watchingUser );
523 
524  # $PAGEEDITDATE is the time and date of the page change
525  # expressed in terms of individual local time of the notification
526  # recipient, i.e. watching user
527  $mwServices = MediaWikiServices::getInstance();
528  $contLang = $mwServices->getContentLanguage();
529  $watchingUserName = (
530  $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
531  $watchingUser->getRealName() !== ''
532  ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
533  $body = str_replace(
534  [
535  '$WATCHINGUSERNAME',
536  '$PAGEEDITDATE',
537  '$PAGEEDITTIME'
538  ],
539  [
540  $watchingUserName,
541  $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
542  $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
543  ],
544  $this->body
545  );
546 
547  $headers = [];
548  if ( $source === self::WATCHLIST ) {
549  $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
550  }
551 
552  return UserMailer::send( $to, $this->from, $this->subject, $body, [
553  'replyTo' => $this->replyto,
554  'headers' => $headers,
555  ] );
556  }
557 
564  private function sendImpersonal( $addresses ) {
565  if ( empty( $addresses ) ) {
566  return null;
567  }
568 
569  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
570  $body = str_replace(
571  [
572  '$WATCHINGUSERNAME',
573  '$PAGEEDITDATE',
574  '$PAGEEDITTIME'
575  ],
576  [
577  wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
578  $contLang->date( $this->timestamp, false, false ),
579  $contLang->time( $this->timestamp, false, false )
580  ],
581  $this->body
582  );
583 
584  return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
585  'replyTo' => $this->replyto,
586  ] );
587  }
588 
589 }
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
Send an email notification.
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
MediaWiki exception.
Definition: MWException.php:30
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:1106
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:1069
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:97
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:591
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition: User.php:2373
getUser()
Definition: User.php:3463
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