MediaWiki master
EmailNotification.php
Go to the documentation of this file.
1<?php
35
62
66 private const USER_TALK = 'user_talk';
70 private const WATCHLIST = 'watchlist';
74 private const ALL_CHANGES = 'all_changes';
75
76 protected string $subject = '';
77
78 protected string $body = '';
79
81
82 protected ?MailAddress $from;
83
84 protected ?string $timestamp;
85
86 protected string $summary = '';
87
88 protected ?bool $minorEdit;
89
91 protected $oldid;
92
93 protected bool $composed_common = false;
94
95 protected string $pageStatus = '';
96
98 protected array $mailTargets = [];
99
100 protected Title $title;
101
102 protected User $editor;
103
114 public function getPageStatus() {
115 return $this->pageStatus;
116 }
117
134 public function notifyOnPageChange(
135 Authority $editor,
136 $title,
137 $timestamp,
138 $summary,
139 $minorEdit,
140 $oldid = false,
141 $pageStatus = 'changed'
142 ): bool {
143 if ( $title->getNamespace() < 0 ) {
144 return false;
145 }
146
147 $mwServices = MediaWikiServices::getInstance();
148 $config = $mwServices->getMainConfig();
149
150 // update wl_notificationtimestamp for watchers
151 $watchers = [];
152 if ( $config->get( MainConfigNames::EnotifWatchlist ) || $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
153 $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
154 $editor->getUser(),
155 $title,
156 $timestamp
157 );
158 }
159
160 // Don't send email for bots
161 if ( $mwServices->getUserFactory()->newFromAuthority( $editor )->isBot() ) {
162 return false;
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 )
180 && $title->getNamespace() === NS_USER_TALK
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
225 Authority $editor,
226 $title,
227 $timestamp,
228 $summary,
229 $minorEdit,
230 $oldid,
231 $watchers,
232 $pageStatus = 'changed'
233 ) {
234 # we use $wgPasswordSender as sender's address
235
236 $mwServices = MediaWikiServices::getInstance();
237 $messageCache = $mwServices->getMessageCache();
238 $config = $mwServices->getMainConfig();
239
240 # The following code is only run, if several conditions are met:
241 # 1. EmailNotification for pages (other than user_talk pages) must be enabled
242 # 2. minor edits (changes) are only regarded if the global flag indicates so
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 $hookRunner = new HookRunner( $mwServices->getHookContainer() );
256 $hookRunner->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
257 if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
258 throw new UnexpectedValueException( 'Not a valid page status!' );
259 }
260
261 $userTalkId = false;
262
263 if ( !$minorEdit ||
264 ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
265 !$editor->isAllowed( 'nominornewtalk' ) )
266 ) {
267 if ( $config->get( MainConfigNames::EnotifUserTalk )
268 && $title->getNamespace() === NS_USER_TALK
269 && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
270 ) {
271 $targetUser = User::newFromName( $title->getText() );
272 $this->compose( $targetUser, self::USER_TALK, $messageCache );
273 $userTalkId = $targetUser->getId();
274 }
275
276 if ( $config->get( MainConfigNames::EnotifWatchlist ) ) {
277 $userOptionsLookup = $mwServices->getUserOptionsLookup();
278 // Send updates to watchers other than the current editor
279 // and don't send to watchers who are blocked and cannot login
280 $userArray = UserArray::newFromIDs( $watchers );
281 foreach ( $userArray as $watchingUser ) {
282 if ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' )
283 && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, 'enotifminoredits' ) )
284 && $watchingUser->isEmailConfirmed()
285 && $watchingUser->getId() != $userTalkId
286 && !in_array( $watchingUser->getName(),
287 $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
288 // @TODO Partial blocks should not prevent the user from logging in.
289 // see: https://phabricator.wikimedia.org/T208895
290 && !( $config->get( MainConfigNames::BlockDisablesLogin ) &&
291 $watchingUser->getBlock() )
292 && $hookRunner->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
293 ) {
294 $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
295 }
296 }
297 }
298 }
299
300 foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) {
301 if ( $editor->getUser()->getName() == $name ) {
302 // No point notifying the user that actually made the change!
303 continue;
304 }
305 $user = User::newFromName( $name );
306 if ( $user instanceof User ) {
307 $this->compose( $user, self::ALL_CHANGES, $messageCache );
308 }
309 }
310 $this->sendMails();
311 }
312
319 private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
320 $services = MediaWikiServices::getInstance();
321 $config = $services->getMainConfig();
322
323 if ( !$config->get( MainConfigNames::EnotifUserTalk ) || $title->getNamespace() !== NS_USER_TALK ) {
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 ( $targetUser->isTemp() ) {
335 wfDebug( __METHOD__ . ": talk page owner is a temporary user so doesn't have email" );
336 } elseif ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
337 $targetUser->getBlock()
338 ) {
339 // @TODO Partial blocks should not prevent the user from logging in.
340 // see: https://phabricator.wikimedia.org/T208895
341 wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
342 } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' )
343 && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) )
344 ) {
345 if ( !$targetUser->isEmailConfirmed() ) {
346 wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
347 } elseif ( !( new HookRunner( $services->getHookContainer() ) )
348 ->onAbortTalkPageEmailNotification( $targetUser, $title )
349 ) {
350 wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" );
351 } else {
352 wfDebug( __METHOD__ . ": sending talk page update notification" );
353 return true;
354 }
355 } else {
356 wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
357 }
358 return false;
359 }
360
364 private function composeCommonMailtext( MessageCache $messageCache ) {
365 $services = MediaWikiServices::getInstance();
366 $config = $services->getMainConfig();
367 $userOptionsLookup = $services->getUserOptionsLookup();
368 $urlUtils = $services->getUrlUtils();
369
370 $this->composed_common = true;
371
372 # You as the WikiAdmin and Sysops can make use of plenty of
373 # named variables when composing your notification emails while
374 # simply editing the Meta pages
375
376 $keys = [];
377 $postTransformKeys = [];
378 $pageTitleUrl = $this->title->getCanonicalURL();
379 $pageTitle = $this->title->getPrefixedText();
380
381 if ( $this->oldid ) {
382 // Always show a link to the diff which triggered the mail. See T34210.
383 $keys['$NEWPAGE'] = "\n\n" . wfMessage(
384 'enotif_lastdiff',
385 $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
386 )->inContentLanguage()->text();
387
388 if ( !$config->get( MainConfigNames::EnotifImpersonal ) ) {
389 // For personal mail, also show a link to the diff of all changes
390 // since last visited.
391 $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
392 'enotif_lastvisited',
393 $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
394 )->inContentLanguage()->text();
395 }
396 $keys['$OLDID'] = $this->oldid;
397 $keys['$PAGELOG'] = '';
398 } else {
399 // If there is no revision to link to, link to the page log, which should have details. See T115183.
400 $keys['$OLDID'] = '';
401 $keys['$NEWPAGE'] = '';
402 $keys['$PAGELOG'] = "\n\n" . wfMessage(
403 'enotif_pagelog',
404 SpecialPage::getTitleFor( 'Log' )->getCanonicalURL( [ 'page' => $this->title->getPrefixedDBkey() ] )
405 )->inContentLanguage()->text();
406
407 }
408
409 $keys['$PAGETITLE'] = $this->title->getPrefixedText();
410 $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
411 $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
412 "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
413 '';
414 $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
415
416 if ( $this->editor->isAnon() ) {
417 # real anon (user:xxx.xxx.xxx.xxx)
418 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
419 ->inContentLanguage()->text();
420 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
421 } elseif ( $this->editor->isTemp() ) {
422 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_temp_editor', $this->editor->getName() )
423 ->inContentLanguage()->text();
424 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
425 } else {
426 $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
427 $this->editor->getRealName() !== ''
428 ? $this->editor->getRealName() : $this->editor->getName();
429 $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
430 $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
431 }
432
433 $keys['$PAGEEDITOR_WIKI'] = $this->editor->getTalkPage()->getCanonicalURL();
434 $keys['$HELPPAGE'] = $urlUtils->expand(
435 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() ),
437 ) ?? false;
438
439 # Replace this after transforming the message, T37019
440 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
441
442 // Now build message's subject and body
443
444 // Messages:
445 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
446 // enotif_subject_restored, enotif_subject_changed
447 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
448 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
449
450 // Messages:
451 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
452 // enotif_body_intro_restored, enotif_body_intro_changed
453 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
454 ->inContentLanguage()
455 ->params( $pageTitle, $keys['$PAGEEDITOR'], "<{$pageTitleUrl}>" )
456 ->text();
457
458 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
459 $body = strtr( $body, $keys );
460 $body = $messageCache->transform( $body, false, null, $this->title );
461 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
462
463 # Reveal the page editor's address as REPLY-TO address only if
464 # the user has not opted-out and the option is enabled at the
465 # global configuration level.
466 $adminAddress = new MailAddress(
467 $config->get( MainConfigNames::PasswordSender ),
468 wfMessage( 'emailsender' )->inContentLanguage()->text()
469 );
470 if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
471 && ( $this->editor->getEmail() != '' )
472 && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
473 ) {
474 $editorAddress = MailAddress::newFromUser( $this->editor );
475 if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
476 $this->from = $editorAddress;
477 } else {
478 $this->from = $adminAddress;
479 $this->replyto = $editorAddress;
480 }
481 } else {
482 $this->from = $adminAddress;
483 $this->replyto = new MailAddress(
484 $config->get( MainConfigNames::NoReplyAddress )
485 );
486 }
487 }
488
498 private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
499 if ( !$this->composed_common ) {
500 $this->composeCommonMailtext( $messageCache );
501 }
502
503 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
504 $this->mailTargets[] = MailAddress::newFromUser( $user );
505 } else {
506 $this->sendPersonalised( $user, $source );
507 }
508 }
509
513 private function sendMails() {
514 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
515 $this->sendImpersonal( $this->mailTargets );
516 }
517 }
518
529 private function sendPersonalised( UserEmailContact $watchingUser, $source ): StatusValue {
530 // From the PHP manual:
531 // Note: The to parameter cannot be an address in the form of
532 // "Something <someone@example.com>". The mail command will not parse
533 // this properly while talking with the MTA.
534 $to = MailAddress::newFromUser( $watchingUser );
535
536 # $PAGEEDITDATE is the time and date of the page change
537 # expressed in terms of individual local time of the notification
538 # recipient, i.e. watching user
539 $mwServices = MediaWikiServices::getInstance();
540 $contLang = $mwServices->getContentLanguage();
541 $watchingUserName = (
542 $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
543 $watchingUser->getRealName() !== ''
544 ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
545 $body = str_replace(
546 [
547 '$WATCHINGUSERNAME',
548 '$PAGEEDITDATE',
549 '$PAGEEDITTIME'
550 ],
551 [
552 $watchingUserName,
553 $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
554 $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
555 ],
556 $this->body
557 );
558
559 $headers = [];
560 if ( $source === self::WATCHLIST ) {
561 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
562 }
563
564 return $mwServices
565 ->getEmailer()
566 ->send(
567 [ $to ],
568 $this->from,
569 $this->subject,
570 $body,
571 null,
572 [
573 'replyTo' => $this->replyto,
574 'headers' => $headers,
575 ]
576 );
577 }
578
585 private function sendImpersonal( array $addresses ): ?StatusValue {
586 if ( count( $addresses ) === 0 ) {
587 return null;
588 }
589 $services = MediaWikiServices::getInstance();
590 $contLang = $services->getContentLanguage();
591 $body = str_replace(
592 [
593 '$WATCHINGUSERNAME',
594 '$PAGEEDITDATE',
595 '$PAGEEDITTIME'
596 ],
597 [
598 wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
599 $contLang->date( $this->timestamp, false, false ),
600 $contLang->time( $this->timestamp, false, false )
601 ],
602 $this->body
603 );
604
605 return $services
606 ->getEmailer()
607 ->send(
608 $addresses,
609 $this->from,
610 $this->subject,
611 $body,
612 null,
613 [
614 'replyTo' => $this->replyto,
615 ]
616 );
617 }
618
619}
const PROTO_CURRENT
Definition Defines.php:215
const NS_USER_TALK
Definition Defines.php:68
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Find watchers and create email notifications after a 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.
Represent and format a single name and email address pair for SMTP.
static newFromUser(UserEmailContact $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:1041
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1014
getOption(UserIdentity $user, string $oname, $defaultOverride=null, bool $ignoreHidden=false, int $queryFlags=IDBAccessObject::READ_NORMAL)
Get the user's current setting for a given option.
Class to walk into a list of User objects.
Definition UserArray.php:33
User class for the MediaWiki software.
Definition User.php:119
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2201
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:1205
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
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
getUser()
Returns the performer of the actions associated with this authority.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
$source