MediaWiki master
EmailNotification.php
Go to the documentation of this file.
1<?php
37
60
64 private const USER_TALK = 'user_talk';
68 private const WATCHLIST = 'watchlist';
72 private const ALL_CHANGES = 'all_changes';
73
74 protected string $subject = '';
75
76 protected string $body = '';
77
79
80 protected ?MailAddress $from;
81
82 protected ?string $timestamp;
83
84 protected string $summary = '';
85
86 protected ?bool $minorEdit;
87
89 protected $oldid;
90
91 protected bool $composed_common = false;
92
93 protected string $pageStatus = '';
94
96 protected array $mailTargets = [];
97
98 protected Title $title;
99
100 protected User $editor;
101
112 public function getPageStatus() {
113 return $this->pageStatus;
114 }
115
131 public function notifyOnPageChange(
132 Authority $editor,
133 $title,
134 $timestamp,
135 $summary,
136 $minorEdit,
137 $oldid = false,
138 $pageStatus = 'changed'
139 ): bool {
140 if ( $title->getNamespace() < 0 ) {
141 return false;
142 }
143
144 $mwServices = MediaWikiServices::getInstance();
145 $config = $mwServices->getMainConfig();
146
147 // update wl_notificationtimestamp for watchers
148 $watchers = [];
149 if ( $config->get( MainConfigNames::EnotifWatchlist ) || $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
150 $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
151 $editor->getUser(),
152 $title,
153 $timestamp
154 );
155 }
156
157 // Don't send email for bots
158 if ( $mwServices->getUserFactory()->newFromAuthority( $editor )->isBot() ) {
159 return false;
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 === [] &&
168 !count( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
169 ) {
170 $sendEmail = false;
171 // Only send notification for non minor edits, unless $wgEnotifMinorEdits
172 if ( !$minorEdit ||
173 ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
174 !$editor->isAllowed( 'nominornewtalk' ) )
175 ) {
176 if ( $config->get( MainConfigNames::EnotifUserTalk )
177 && $title->getNamespace() === NS_USER_TALK
178 && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
179 ) {
180 $sendEmail = true;
181 }
182 }
183 }
184
185 if ( $sendEmail ) {
186 $mwServices->getJobQueueGroup()->lazyPush( new EnotifNotifyJob(
187 $title,
188 [
189 'editor' => $editor->getUser()->getName(),
190 'editorID' => $editor->getUser()->getId(),
191 'timestamp' => $timestamp,
192 'summary' => $summary,
193 'minorEdit' => $minorEdit,
194 'oldid' => $oldid,
195 'watchers' => $watchers,
196 'pageStatus' => $pageStatus
197 ]
198 ) );
199 }
200
201 return $sendEmail;
202 }
203
220 Authority $editor,
221 $title,
222 $timestamp,
223 $summary,
224 $minorEdit,
225 $oldid,
226 $watchers,
227 $pageStatus = 'changed'
228 ) {
229 # we use $wgPasswordSender as sender's address
230
231 $mwServices = MediaWikiServices::getInstance();
232 $messageCache = $mwServices->getMessageCache();
233 $config = $mwServices->getMainConfig();
234
235 # The following code is only run, if several conditions are met:
236 # 1. EmailNotification for pages (other than user_talk pages) must be enabled
237 # 2. minor edits (changes) are only regarded if the global flag indicates so
238
239 $this->title = $title;
240 $this->timestamp = $timestamp;
241 $this->summary = $summary;
242 $this->minorEdit = $minorEdit;
243 $this->oldid = $oldid;
244 $this->editor = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $editor );
245 $this->composed_common = false;
246 $this->pageStatus = $pageStatus;
247
248 $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
249
250 $hookRunner = new HookRunner( $mwServices->getHookContainer() );
251 $hookRunner->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
252 if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
253 throw new UnexpectedValueException( 'Not a valid page status!' );
254 }
255
256 $userTalkId = false;
257
258 if ( !$minorEdit ||
259 ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
260 !$editor->isAllowed( 'nominornewtalk' ) )
261 ) {
262 if ( $config->get( MainConfigNames::EnotifUserTalk )
263 && $title->getNamespace() === NS_USER_TALK
264 && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
265 ) {
266 $targetUser = User::newFromName( $title->getText() );
267 $this->compose( $targetUser, self::USER_TALK, $messageCache );
268 $userTalkId = $targetUser->getId();
269 }
270
271 if ( $config->get( MainConfigNames::EnotifWatchlist ) ) {
272 $userOptionsLookup = $mwServices->getUserOptionsLookup();
273 // Send updates to watchers other than the current editor
274 // and don't send to watchers who are blocked and cannot login
275 $userArray = UserArray::newFromIDs( $watchers );
276 foreach ( $userArray as $watchingUser ) {
277 if ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' )
278 && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, 'enotifminoredits' ) )
279 && $watchingUser->isEmailConfirmed()
280 && $watchingUser->getId() != $userTalkId
281 && !in_array( $watchingUser->getName(),
282 $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
283 // @TODO Partial blocks should not prevent the user from logging in.
284 // see: https://phabricator.wikimedia.org/T208895
285 && !( $config->get( MainConfigNames::BlockDisablesLogin ) &&
286 $watchingUser->getBlock() )
287 && $hookRunner->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
288 ) {
289 $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
290 }
291 }
292 }
293 }
294
295 foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) {
296 if ( $editor->getUser()->getName() == $name ) {
297 // No point notifying the user that actually made the change!
298 continue;
299 }
300 $user = User::newFromName( $name );
301 if ( $user instanceof User ) {
302 $this->compose( $user, self::ALL_CHANGES, $messageCache );
303 }
304 }
305 $this->sendMails();
306 }
307
314 private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
315 $services = MediaWikiServices::getInstance();
316 $config = $services->getMainConfig();
317
318 if ( !$config->get( MainConfigNames::EnotifUserTalk ) || $title->getNamespace() !== NS_USER_TALK ) {
319 return false;
320 }
321
322 $userOptionsLookup = $services->getUserOptionsLookup();
323 $targetUser = User::newFromName( $title->getText() );
324
325 if ( !$targetUser || $targetUser->isAnon() ) {
326 wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
327 } elseif ( $targetUser->getId() == $editor->getId() ) {
328 wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
329 } elseif ( $targetUser->isTemp() ) {
330 wfDebug( __METHOD__ . ": talk page owner is a temporary user so doesn't have email" );
331 } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
332 $targetUser->getBlock()
333 ) {
334 // @TODO Partial blocks should not prevent the user from logging in.
335 // see: https://phabricator.wikimedia.org/T208895
336 wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
337 } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' )
338 && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) )
339 ) {
340 if ( !$targetUser->isEmailConfirmed() ) {
341 wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
342 } elseif ( !( new HookRunner( $services->getHookContainer() ) )
343 ->onAbortTalkPageEmailNotification( $targetUser, $title )
344 ) {
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 $keys['$PAGELOG'] = '';
393 } else {
394 // If there is no revision to link to, link to the page log, which should have details. See T115183.
395 $keys['$OLDID'] = '';
396 $keys['$NEWPAGE'] = '';
397 $keys['$PAGELOG'] = "\n\n" . wfMessage(
398 'enotif_pagelog',
399 SpecialPage::getTitleFor( 'Log' )->getCanonicalURL( [ 'page' => $this->title->getPrefixedDBkey() ] )
400 )->inContentLanguage()->text();
401
402 }
403
404 $keys['$PAGETITLE'] = $this->title->getPrefixedText();
405 $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
406 $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
407 "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
408 '';
409 $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
410
411 if ( $this->editor->isAnon() ) {
412 # real anon (user:xxx.xxx.xxx.xxx)
413 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
414 ->inContentLanguage()->text();
415 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
416 } elseif ( $this->editor->isTemp() ) {
417 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_temp_editor', $this->editor->getName() )
418 ->inContentLanguage()->text();
419 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
420 } else {
421 $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
422 $this->editor->getRealName() !== ''
423 ? $this->editor->getRealName() : $this->editor->getName();
424 $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
425 $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
426 }
427
428 $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
429 $keys['$HELPPAGE'] = wfExpandUrl(
430 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
431 );
432
433 # Replace this after transforming the message, T37019
434 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
435
436 // Now build message's subject and body
437
438 // Messages:
439 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
440 // enotif_subject_restored, enotif_subject_changed
441 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
442 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
443
444 // Messages:
445 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
446 // enotif_body_intro_restored, enotif_body_intro_changed
447 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
448 ->inContentLanguage()
449 ->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
450 ->text();
451
452 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
453 $body = strtr( $body, $keys );
454 $body = $messageCache->transform( $body, false, null, $this->title );
455 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
456
457 # Reveal the page editor's address as REPLY-TO address only if
458 # the user has not opted-out and the option is enabled at the
459 # global configuration level.
460 $adminAddress = new MailAddress(
461 $config->get( MainConfigNames::PasswordSender ),
462 wfMessage( 'emailsender' )->inContentLanguage()->text()
463 );
464 if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
465 && ( $this->editor->getEmail() != '' )
466 && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
467 ) {
468 $editorAddress = MailAddress::newFromUser( $this->editor );
469 if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
470 $this->from = $editorAddress;
471 } else {
472 $this->from = $adminAddress;
473 $this->replyto = $editorAddress;
474 }
475 } else {
476 $this->from = $adminAddress;
477 $this->replyto = new MailAddress(
478 $config->get( MainConfigNames::NoReplyAddress )
479 );
480 }
481 }
482
492 private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
493 if ( !$this->composed_common ) {
494 $this->composeCommonMailtext( $messageCache );
495 }
496
497 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
498 $this->mailTargets[] = MailAddress::newFromUser( $user );
499 } else {
500 $this->sendPersonalised( $user, $source );
501 }
502 }
503
507 private function sendMails() {
508 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
509 $this->sendImpersonal( $this->mailTargets );
510 }
511 }
512
523 private function sendPersonalised( UserEmailContact $watchingUser, $source ): StatusValue {
524 // From the PHP manual:
525 // Note: The to parameter cannot be an address in the form of
526 // "Something <someone@example.com>". The mail command will not parse
527 // this properly while talking with the MTA.
528 $to = MailAddress::newFromUser( $watchingUser );
529
530 # $PAGEEDITDATE is the time and date of the page change
531 # expressed in terms of individual local time of the notification
532 # recipient, i.e. watching user
533 $mwServices = MediaWikiServices::getInstance();
534 $contLang = $mwServices->getContentLanguage();
535 $watchingUserName = (
536 $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
537 $watchingUser->getRealName() !== ''
538 ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
539 $body = str_replace(
540 [
541 '$WATCHINGUSERNAME',
542 '$PAGEEDITDATE',
543 '$PAGEEDITTIME'
544 ],
545 [
546 $watchingUserName,
547 $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
548 $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
549 ],
550 $this->body
551 );
552
553 $headers = [];
554 if ( $source === self::WATCHLIST ) {
555 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
556 }
557
558 return $mwServices
559 ->getEmailer()
560 ->send(
561 [ $to ],
562 $this->from,
563 $this->subject,
564 $body,
565 null,
566 [
567 'replyTo' => $this->replyto,
568 'headers' => $headers,
569 ]
570 );
571 }
572
579 private function sendImpersonal( array $addresses ): ?StatusValue {
580 if ( count( $addresses ) === 0 ) {
581 return null;
582 }
583 $services = MediaWikiServices::getInstance();
584 $contLang = $services->getContentLanguage();
585 $body = str_replace(
586 [
587 '$WATCHINGUSERNAME',
588 '$PAGEEDITDATE',
589 '$PAGEEDITTIME'
590 ],
591 [
592 wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
593 $contLang->date( $this->timestamp, false, false ),
594 $contLang->time( $this->timestamp, false, false )
595 ],
596 $this->body
597 );
598
599 return $services
600 ->getEmailer()
601 ->send(
602 $addresses,
603 $this->from,
604 $this->subject,
605 $body,
606 null,
607 [
608 'replyTo' => $this->replyto,
609 ]
610 );
611 }
612
613}
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:81
This module processes the email notifications when the current page is changed.
actuallyNotifyOnPageChange(Authority $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus='changed')
Immediate version of notifyOnPageChange().
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 ...
array MailAddress[] $mailTargets
Send an email notification.
Stores a single person's name and email address.
static newFromUser(UserEmailContact $user)
Create a new MailAddress object for the given user.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Parent class for all special pages.
Represents a title within MediaWiki.
Definition Title.php:78
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1044
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1017
internal since 1.36
Definition User.php:93
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2291
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:1176
Generic operation result class Has warning/error list, boolean status and arbitrary value.
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 with the current execution context,...
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