MediaWiki REL1_39
EmailNotification.php
Go to the documentation of this file.
1<?php
32
55
59 private const USER_TALK = 'user_talk';
63 private const WATCHLIST = 'watchlist';
67 private const ALL_CHANGES = 'all_changes';
68
70 protected $subject = '';
71
73 protected $body = '';
74
76 protected $replyto;
77
79 protected $from;
80
82 protected $timestamp;
83
85 protected $summary = '';
86
88 protected $minorEdit;
89
91 protected $oldid;
92
94 protected $composed_common = false;
95
97 protected $pageStatus = '';
98
100 protected $mailTargets = [];
101
103 protected $title;
104
106 protected $editor;
107
118 public function getPageStatus() {
119 return $this->pageStatus;
120 }
121
137 public function notifyOnPageChange(
138 Authority $editor,
139 $title,
140 $timestamp,
141 $summary,
142 $minorEdit,
143 $oldid = false,
144 $pageStatus = 'changed'
145 ): bool {
146 if ( $title->getNamespace() < 0 ) {
147 return false;
148 }
149
150 $mwServices = MediaWikiServices::getInstance();
151 $config = $mwServices->getMainConfig();
152
153 // update wl_notificationtimestamp for watchers
154 $watchers = [];
155 if ( $config->get( MainConfigNames::EnotifWatchlist ) ||
156 $config->get( MainConfigNames::ShowUpdatedMarker ) ) {
157 $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
158 $editor->getUser(),
159 $title,
160 $timestamp
161 );
162 }
163
164 $sendEmail = true;
165 // $watchers deals with $wgEnotifWatchlist.
166 // If nobody is watching the page, and there are no users notified on all changes
167 // don't bother creating a job/trying to send emails, unless it's a
168 // talk page with an applicable notification.
169 if ( $watchers === [] &&
170 !count( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) ) ) {
171 $sendEmail = false;
172 // Only send notification for non minor edits, unless $wgEnotifMinorEdits
173 if ( !$minorEdit ||
174 ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
175 !$editor->isAllowed( 'nominornewtalk' ) )
176 ) {
177 $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
178 if ( $config->get( MainConfigNames::EnotifUserTalk )
179 && $isUserTalkPage
180 && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
181 ) {
182 $sendEmail = true;
183 }
184 }
185 }
186
187 if ( $sendEmail ) {
188 $mwServices->getJobQueueGroup()->lazyPush( new EnotifNotifyJob(
189 $title,
190 [
191 'editor' => $editor->getUser()->getName(),
192 'editorID' => $editor->getUser()->getId(),
193 'timestamp' => $timestamp,
194 'summary' => $summary,
195 'minorEdit' => $minorEdit,
196 'oldid' => $oldid,
197 'watchers' => $watchers,
198 'pageStatus' => $pageStatus
199 ]
200 ) );
201 }
202
203 return $sendEmail;
204 }
205
223 Authority $editor,
224 $title,
225 $timestamp,
226 $summary,
227 $minorEdit,
228 $oldid,
229 $watchers,
230 $pageStatus = 'changed'
231 ) {
232 # we use $wgPasswordSender as sender's address
233
234 $mwServices = MediaWikiServices::getInstance();
235 $messageCache = $mwServices->getMessageCache();
236 $config = $mwServices->getMainConfig();
237
238 # The following code is only run, if several conditions are met:
239 # 1. EmailNotification for pages (other than user_talk pages) must be enabled
240 # 2. minor edits (changes) are only regarded if the global flag indicates so
241
242 $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
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 Hooks::runner()->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
256 if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
257 throw new MWException( 'Not a valid page status!' );
258 }
259
260 $userTalkId = false;
261
262 if ( !$minorEdit ||
263 ( $config->get( MainConfigNames::EnotifMinorEdits ) &&
264 !$editor->isAllowed( 'nominornewtalk' ) )
265 ) {
266 if ( $config->get( MainConfigNames::EnotifUserTalk )
267 && $isUserTalkPage
268 && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
269 ) {
270 $targetUser = User::newFromName( $title->getText() );
271 $this->compose( $targetUser, self::USER_TALK, $messageCache );
272 $userTalkId = $targetUser->getId();
273 }
274
275 if ( $config->get( MainConfigNames::EnotifWatchlist ) ) {
276 $userOptionsLookup = $mwServices->getUserOptionsLookup();
277 // Send updates to watchers other than the current editor
278 // and don't send to watchers who are blocked and cannot login
279 $userArray = UserArray::newFromIDs( $watchers );
280 foreach ( $userArray as $watchingUser ) {
281 if ( $userOptionsLookup->getOption( $watchingUser, 'enotifwatchlistpages' )
282 && ( !$minorEdit || $userOptionsLookup->getOption( $watchingUser, 'enotifminoredits' ) )
283 && $watchingUser->isEmailConfirmed()
284 && $watchingUser->getId() != $userTalkId
285 && !in_array( $watchingUser->getName(),
286 $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) )
287 // @TODO Partial blocks should not prevent the user from logging in.
288 // see: https://phabricator.wikimedia.org/T208895
289 && !( $config->get( MainConfigNames::BlockDisablesLogin ) &&
290 $watchingUser->getBlock() )
291 && Hooks::runner()->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
292 ) {
293 $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
294 }
295 }
296 }
297 }
298
299 foreach ( $config->get( MainConfigNames::UsersNotifiedOnAllChanges ) as $name ) {
300 if ( $editor->getUser()->getName() == $name ) {
301 // No point notifying the user that actually made the change!
302 continue;
303 }
304 $user = User::newFromName( $name );
305 if ( $user instanceof User ) {
306 $this->compose( $user, self::ALL_CHANGES, $messageCache );
307 }
308 }
309 $this->sendMails();
310 }
311
318 private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
319 $services = MediaWikiServices::getInstance();
320 $config = $services->getMainConfig();
321 $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
322
323 if ( !$config->get( MainConfigNames::EnotifUserTalk ) || !$isUserTalkPage ) {
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 ( $config->get( MainConfigNames::BlockDisablesLogin ) &&
335 $targetUser->getBlock() ) {
336 // @TODO Partial blocks should not prevent the user from logging in.
337 // see: https://phabricator.wikimedia.org/T208895
338 wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
339 } elseif ( $userOptionsLookup->getOption( $targetUser, 'enotifusertalkpages' )
340 && ( !$minorEdit || $userOptionsLookup->getOption( $targetUser, 'enotifminoredits' ) )
341 ) {
342 if ( !$targetUser->isEmailConfirmed() ) {
343 wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
344 } elseif ( !Hooks::runner()->onAbortTalkPageEmailNotification( $targetUser, $title ) ) {
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 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
393 $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
394 } else {
395 # clear $OLDID placeholder in the message template
396 $keys['$OLDID'] = '';
397 $keys['$NEWPAGE'] = '';
398 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
399 $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
400 }
401
402 $keys['$PAGETITLE'] = $this->title->getPrefixedText();
403 $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
404 $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
405 "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
406 '';
407 $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
408
409 if ( !$this->editor->isRegistered() ) {
410 # real anon (user:xxx.xxx.xxx.xxx)
411 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
412 ->inContentLanguage()->text();
413 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
414
415 } else {
416 $keys['$PAGEEDITOR'] = $config->get( MainConfigNames::EnotifUseRealName ) &&
417 $this->editor->getRealName() !== ''
418 ? $this->editor->getRealName() : $this->editor->getName();
419 $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
420 $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
421 }
422
423 $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
424 $keys['$HELPPAGE'] = wfExpandUrl(
425 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
426 );
427
428 # Replace this after transforming the message, T37019
429 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
430
431 // Now build message's subject and body
432
433 // Messages:
434 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
435 // enotif_subject_restored, enotif_subject_changed
436 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
437 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
438
439 // Messages:
440 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
441 // enotif_body_intro_restored, enotif_body_intro_changed
442 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
443 ->inContentLanguage()
444 ->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
445 ->text();
446
447 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
448 $body = strtr( $body, $keys );
449 $body = $messageCache->transform( $body, false, null, $this->title );
450 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
451
452 # Reveal the page editor's address as REPLY-TO address only if
453 # the user has not opted-out and the option is enabled at the
454 # global configuration level.
455 $adminAddress = new MailAddress(
456 $config->get( MainConfigNames::PasswordSender ),
457 wfMessage( 'emailsender' )->inContentLanguage()->text()
458 );
459 if ( $config->get( MainConfigNames::EnotifRevealEditorAddress )
460 && ( $this->editor->getEmail() != '' )
461 && $userOptionsLookup->getOption( $this->editor, 'enotifrevealaddr' )
462 ) {
463 $editorAddress = MailAddress::newFromUser( $this->editor );
464 if ( $config->get( MainConfigNames::EnotifFromEditor ) ) {
465 $this->from = $editorAddress;
466 } else {
467 $this->from = $adminAddress;
468 $this->replyto = $editorAddress;
469 }
470 } else {
471 $this->from = $adminAddress;
472 $this->replyto = new MailAddress(
473 $config->get( MainConfigNames::NoReplyAddress )
474 );
475 }
476 }
477
487 private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
488 if ( !$this->composed_common ) {
489 $this->composeCommonMailtext( $messageCache );
490 }
491
492 if ( MediaWikiServices::getInstance()->getMainConfig()
493 ->get( MainConfigNames::EnotifImpersonal ) ) {
494 $this->mailTargets[] = MailAddress::newFromUser( $user );
495 } else {
496 $this->sendPersonalised( $user, $source );
497 }
498 }
499
503 private function sendMails() {
504 if ( MediaWikiServices::getInstance()->getMainConfig()
505 ->get( MainConfigNames::EnotifImpersonal ) ) {
506 $this->sendImpersonal( $this->mailTargets );
507 }
508 }
509
520 private function sendPersonalised( UserEmailContact $watchingUser, $source ) {
521 // From the PHP manual:
522 // Note: The to parameter cannot be an address in the form of
523 // "Something <someone@example.com>". The mail command will not parse
524 // this properly while talking with the MTA.
525 $to = MailAddress::newFromUser( $watchingUser );
526
527 # $PAGEEDITDATE is the time and date of the page change
528 # expressed in terms of individual local time of the notification
529 # recipient, i.e. watching user
530 $mwServices = MediaWikiServices::getInstance();
531 $contLang = $mwServices->getContentLanguage();
532 $watchingUserName = (
533 $mwServices->getMainConfig()->get( MainConfigNames::EnotifUseRealName ) &&
534 $watchingUser->getRealName() !== ''
535 ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
536 $body = str_replace(
537 [
538 '$WATCHINGUSERNAME',
539 '$PAGEEDITDATE',
540 '$PAGEEDITTIME'
541 ],
542 [
543 $watchingUserName,
544 $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
545 $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
546 ],
547 $this->body
548 );
549
550 $headers = [];
551 if ( $source === self::WATCHLIST ) {
552 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
553 }
554
555 return UserMailer::send( $to, $this->from, $this->subject, $body, [
556 'replyTo' => $this->replyto,
557 'headers' => $headers,
558 ] );
559 }
560
567 private function sendImpersonal( $addresses ) {
568 if ( empty( $addresses ) ) {
569 return null;
570 }
571
572 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
573 $body = str_replace(
574 [
575 '$WATCHINGUSERNAME',
576 '$PAGEEDITDATE',
577 '$PAGEEDITTIME'
578 ],
579 [
580 wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
581 $contLang->date( $this->timestamp, false, false ),
582 $contLang->time( $this->timestamp, false, false )
583 ],
584 $this->body
585 );
586
587 return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
588 'replyTo' => $this->replyto,
589 ] );
590 }
591
592}
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.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
This module processes the email notifications when the current page is changed.
MailAddress null $replyto
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 ...
MailAddress null $from
MailAddress[] $mailTargets
Job for email notification mails.
MediaWiki exception.
Stores a single person's name and email address.
static newFromUser(UserEmailContact $user)
Create a new MailAddress object for the given user.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
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:1146
static getSafeTitleFor( $name, $subpage=false)
Get a localised Title object for a page name with a possibly unvalidated subpage.
Represents a title within MediaWiki.
Definition Title.php:49
static newFromIDs( $ids)
Definition UserArray.php:52
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...
internal since 1.36
Definition User.php:70
static newFromName( $name, $validate='valid')
Definition User.php:598
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition User.php:2366
getUser()
Definition User.php:3461
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 the current execution context, such as a web reque...
Definition Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
$source