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