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
365 private function composeCommonMailtext( MessageCache $messageCache ) {
366 $services = MediaWikiServices::getInstance();
367 $config = $services->getMainConfig();
368 $userOptionsLookup = $services->getUserOptionsLookup();
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'] = wfExpandUrl(
435 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
436 );
437
438 # Replace this after transforming the message, T37019
439 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
440
441 // Now build message's subject and body
442
443 // Messages:
444 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
445 // enotif_subject_restored, enotif_subject_changed
446 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
447 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
448
449 // Messages:
450 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
451 // enotif_body_intro_restored, enotif_body_intro_changed
452 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
453 ->inContentLanguage()
454 ->params( $pageTitle, $keys['$PAGEEDITOR'], "<{$pageTitleUrl}>" )
455 ->text();
456
457 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
458 $body = strtr( $body, $keys );
459 $body = $messageCache->transform( $body, false, null, $this->title );
460 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
461
462 # Reveal the page editor's address as REPLY-TO address only if
463 # the user has not opted-out and the option is enabled at the
464 # global configuration level.
465 $adminAddress = new MailAddress(
466 $config->get( MainConfigNames::PasswordSender ),
467 wfMessage( 'emailsender' )->inContentLanguage()->text()
468 );
469 if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
470 && ( $this->editor->getEmail() != '' )
471 && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
472 ) {
473 $editorAddress = MailAddress::newFromUser( $this->editor );
474 if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
475 $this->from = $editorAddress;
476 } else {
477 $this->from = $adminAddress;
478 $this->replyto = $editorAddress;
479 }
480 } else {
481 $this->from = $adminAddress;
482 $this->replyto = new MailAddress(
483 $config->get( MainConfigNames::NoReplyAddress )
484 );
485 }
486 }
487
497 private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
498 if ( !$this->composed_common ) {
499 $this->composeCommonMailtext( $messageCache );
500 }
501
502 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
503 $this->mailTargets[] = MailAddress::newFromUser( $user );
504 } else {
505 $this->sendPersonalised( $user, $source );
506 }
507 }
508
512 private function sendMails() {
513 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
514 $this->sendImpersonal( $this->mailTargets );
515 }
516 }
517
528 private function sendPersonalised( UserEmailContact $watchingUser, $source ): StatusValue {
529 // From the PHP manual:
530 // Note: The to parameter cannot be an address in the form of
531 // "Something <someone@example.com>". The mail command will not parse
532 // this properly while talking with the MTA.
533 $to = MailAddress::newFromUser( $watchingUser );
534
535 # $PAGEEDITDATE is the time and date of the page change
536 # expressed in terms of individual local time of the notification
537 # recipient, i.e. watching user
538 $mwServices = MediaWikiServices::getInstance();
539 $contLang = $mwServices->getContentLanguage();
540 $watchingUserName = (
541 $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
542 $watchingUser->getRealName() !== ''
543 ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
544 $body = str_replace(
545 [
546 '$WATCHINGUSERNAME',
547 '$PAGEEDITDATE',
548 '$PAGEEDITTIME'
549 ],
550 [
551 $watchingUserName,
552 $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
553 $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
554 ],
555 $this->body
556 );
557
558 $headers = [];
559 if ( $source === self::WATCHLIST ) {
560 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
561 }
562
563 return $mwServices
564 ->getEmailer()
565 ->send(
566 [ $to ],
567 $this->from,
568 $this->subject,
569 $body,
570 null,
571 [
572 'replyTo' => $this->replyto,
573 'headers' => $headers,
574 ]
575 );
576 }
577
584 private function sendImpersonal( array $addresses ): ?StatusValue {
585 if ( count( $addresses ) === 0 ) {
586 return null;
587 }
588 $services = MediaWikiServices::getInstance();
589 $contLang = $services->getContentLanguage();
590 $body = str_replace(
591 [
592 '$WATCHINGUSERNAME',
593 '$PAGEEDITDATE',
594 '$PAGEEDITTIME'
595 ],
596 [
597 wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
598 $contLang->date( $this->timestamp, false, false ),
599 $contLang->time( $this->timestamp, false, false )
600 ],
601 $this->body
602 );
603
604 return $services
605 ->getEmailer()
606 ->send(
607 $addresses,
608 $this->from,
609 $this->subject,
610 $body,
611 null,
612 [
613 'replyTo' => $this->replyto,
614 ]
615 );
616 }
617
618}
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.
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
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:79
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1045
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1018
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:2190
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:1153
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