MediaWiki 1.43.1
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 $urlUtils = $services->getUrlUtils();
370
371 $this->composed_common = true;
372
373 # You as the WikiAdmin and Sysops can make use of plenty of
374 # named variables when composing your notification emails while
375 # simply editing the Meta pages
376
377 $keys = [];
378 $postTransformKeys = [];
379 $pageTitleUrl = $this->title->getCanonicalURL();
380 $pageTitle = $this->title->getPrefixedText();
381
382 if ( $this->oldid ) {
383 // Always show a link to the diff which triggered the mail. See T34210.
384 $keys['$NEWPAGE'] = "\n\n" . wfMessage(
385 'enotif_lastdiff',
386 $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
387 )->inContentLanguage()->text();
388
389 if ( !$config->get( MainConfigNames::EnotifImpersonal ) ) {
390 // For personal mail, also show a link to the diff of all changes
391 // since last visited.
392 $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
393 'enotif_lastvisited',
394 $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
395 )->inContentLanguage()->text();
396 }
397 $keys['$OLDID'] = $this->oldid;
398 $keys['$PAGELOG'] = '';
399 } else {
400 // If there is no revision to link to, link to the page log, which should have details. See T115183.
401 $keys['$OLDID'] = '';
402 $keys['$NEWPAGE'] = '';
403 $keys['$PAGELOG'] = "\n\n" . wfMessage(
404 'enotif_pagelog',
405 SpecialPage::getTitleFor( 'Log' )->getCanonicalURL( [ 'page' => $this->title->getPrefixedDBkey() ] )
406 )->inContentLanguage()->text();
407
408 }
409
410 $keys['$PAGETITLE'] = $this->title->getPrefixedText();
411 $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
412 $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
413 "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
414 '';
415 $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
416
417 if ( $this->editor->isAnon() ) {
418 # real anon (user:xxx.xxx.xxx.xxx)
419 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
420 ->inContentLanguage()->text();
421 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
422 } elseif ( $this->editor->isTemp() ) {
423 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_temp_editor', $this->editor->getName() )
424 ->inContentLanguage()->text();
425 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
426 } else {
427 $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
428 $this->editor->getRealName() !== ''
429 ? $this->editor->getRealName() : $this->editor->getName();
430 $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
431 $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
432 }
433
434 $keys['$PAGEEDITOR_WIKI'] = $this->editor->getTalkPage()->getCanonicalURL();
435 $keys['$HELPPAGE'] = $urlUtils->expand(
436 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() ),
438 ) ?? false;
439
440 # Replace this after transforming the message, T37019
441 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
442
443 // Now build message's subject and body
444
445 // Messages:
446 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
447 // enotif_subject_restored, enotif_subject_changed
448 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
449 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
450
451 // Messages:
452 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
453 // enotif_body_intro_restored, enotif_body_intro_changed
454 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
455 ->inContentLanguage()
456 ->params( $pageTitle, $keys['$PAGEEDITOR'], "<{$pageTitleUrl}>" )
457 ->text();
458
459 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
460 $body = strtr( $body, $keys );
461 $body = $messageCache->transform( $body, false, null, $this->title );
462 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
463
464 # Reveal the page editor's address as REPLY-TO address only if
465 # the user has not opted-out and the option is enabled at the
466 # global configuration level.
467 $adminAddress = new MailAddress(
468 $config->get( MainConfigNames::PasswordSender ),
469 wfMessage( 'emailsender' )->inContentLanguage()->text()
470 );
471 if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
472 && ( $this->editor->getEmail() != '' )
473 && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
474 ) {
475 $editorAddress = MailAddress::newFromUser( $this->editor );
476 if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
477 $this->from = $editorAddress;
478 } else {
479 $this->from = $adminAddress;
480 $this->replyto = $editorAddress;
481 }
482 } else {
483 $this->from = $adminAddress;
484 $this->replyto = new MailAddress(
485 $config->get( MainConfigNames::NoReplyAddress )
486 );
487 }
488 }
489
499 private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
500 if ( !$this->composed_common ) {
501 $this->composeCommonMailtext( $messageCache );
502 }
503
504 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
505 $this->mailTargets[] = MailAddress::newFromUser( $user );
506 } else {
507 $this->sendPersonalised( $user, $source );
508 }
509 }
510
514 private function sendMails() {
515 if ( MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnotifImpersonal ) ) {
516 $this->sendImpersonal( $this->mailTargets );
517 }
518 }
519
530 private function sendPersonalised( UserEmailContact $watchingUser, $source ): StatusValue {
531 // From the PHP manual:
532 // Note: The to parameter cannot be an address in the form of
533 // "Something <someone@example.com>". The mail command will not parse
534 // this properly while talking with the MTA.
535 $to = MailAddress::newFromUser( $watchingUser );
536
537 # $PAGEEDITDATE is the time and date of the page change
538 # expressed in terms of individual local time of the notification
539 # recipient, i.e. watching user
540 $mwServices = MediaWikiServices::getInstance();
541 $contLang = $mwServices->getContentLanguage();
542 $watchingUserName = (
543 $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
544 $watchingUser->getRealName() !== ''
545 ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
546 $body = str_replace(
547 [
548 '$WATCHINGUSERNAME',
549 '$PAGEEDITDATE',
550 '$PAGEEDITTIME'
551 ],
552 [
553 $watchingUserName,
554 $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
555 $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
556 ],
557 $this->body
558 );
559
560 $headers = [];
561 if ( $source === self::WATCHLIST ) {
562 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
563 }
564
565 return $mwServices
566 ->getEmailer()
567 ->send(
568 [ $to ],
569 $this->from,
570 $this->subject,
571 $body,
572 null,
573 [
574 'replyTo' => $this->replyto,
575 'headers' => $headers,
576 ]
577 );
578 }
579
586 private function sendImpersonal( array $addresses ): ?StatusValue {
587 if ( count( $addresses ) === 0 ) {
588 return null;
589 }
590 $services = MediaWikiServices::getInstance();
591 $contLang = $services->getContentLanguage();
592 $body = str_replace(
593 [
594 '$WATCHINGUSERNAME',
595 '$PAGEEDITDATE',
596 '$PAGEEDITTIME'
597 ],
598 [
599 wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
600 $contLang->date( $this->timestamp, false, false ),
601 $contLang->time( $this->timestamp, false, false )
602 ],
603 $this->body
604 );
605
606 return $services
607 ->getEmailer()
608 ->send(
609 $addresses,
610 $this->from,
611 $this->subject,
612 $body,
613 null,
614 [
615 'replyTo' => $this->replyto,
616 ]
617 );
618 }
619
620}
const PROTO_CURRENT
Definition Defines.php:209
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:1043
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1016
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.
internal since 1.36
Definition User.php:94
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Definition User.php:2181
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:1195
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