MediaWiki REL1_37
EmailNotification.php
Go to the documentation of this file.
1<?php
31
54
58 private const USER_TALK = 'user_talk';
62 private const WATCHLIST = 'watchlist';
66 private const ALL_CHANGES = 'all_changes';
67
69 protected $subject = '';
70
72 protected $body = '';
73
75 protected $replyto;
76
78 protected $from;
79
81 protected $timestamp;
82
84 protected $summary = '';
85
87 protected $minorEdit;
88
90 protected $oldid;
91
93 protected $composed_common = false;
94
96 protected $pageStatus = '';
97
99 protected $mailTargets = [];
100
102 protected $title;
103
105 protected $editor;
106
117 public function getPageStatus() {
118 return $this->pageStatus;
119 }
120
136 public function notifyOnPageChange(
137 Authority $editor,
138 $title,
139 $timestamp,
140 $summary,
141 $minorEdit,
142 $oldid = false,
143 $pageStatus = 'changed'
144 ): bool {
145 if ( $title->getNamespace() < 0 ) {
146 return false;
147 }
148
149 $mwServices = MediaWikiServices::getInstance();
150 $config = $mwServices->getMainConfig();
151
152 // update wl_notificationtimestamp for watchers
153 $watchers = [];
154 if ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) {
155 $watchers = $mwServices->getWatchedItemStore()->updateNotificationTimestamp(
156 $editor->getUser(),
157 $title,
158 $timestamp
159 );
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 === [] && !count( $config->get( 'UsersNotifiedOnAllChanges' ) ) ) {
168 $sendEmail = false;
169 // Only send notification for non minor edits, unless $wgEnotifMinorEdits
170 if ( !$minorEdit ||
171 ( $config->get( 'EnotifMinorEdits' ) && !$editor->isAllowed( 'nominornewtalk' ) )
172 ) {
173 $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
174 if ( $config->get( 'EnotifUserTalk' )
175 && $isUserTalkPage
176 && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
177 ) {
178 $sendEmail = true;
179 }
180 }
181 }
182
183 if ( $sendEmail ) {
185 $title,
186 [
187 'editor' => $editor->getUser()->getName(),
188 'editorID' => $editor->getUser()->getId(),
189 'timestamp' => $timestamp,
190 'summary' => $summary,
191 'minorEdit' => $minorEdit,
192 'oldid' => $oldid,
193 'watchers' => $watchers,
194 'pageStatus' => $pageStatus
195 ]
196 ) );
197 }
198
199 return $sendEmail;
200 }
201
219 Authority $editor,
220 $title,
221 $timestamp,
222 $summary,
223 $minorEdit,
224 $oldid,
225 $watchers,
226 $pageStatus = 'changed'
227 ) {
228 # we use $wgPasswordSender as sender's address
229
230 $mwServices = MediaWikiServices::getInstance();
231 $messageCache = $mwServices->getMessageCache();
232 $config = $mwServices->getMainConfig();
233
234 # The following code is only run, if several conditions are met:
235 # 1. EmailNotification for pages (other than user_talk pages) must be enabled
236 # 2. minor edits (changes) are only regarded if the global flag indicates so
237
238 $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
239
240 $this->title = $title;
241 $this->timestamp = $timestamp;
242 $this->summary = $summary;
243 $this->minorEdit = $minorEdit;
244 $this->oldid = $oldid;
245 $this->editor = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $editor );
246 $this->composed_common = false;
247 $this->pageStatus = $pageStatus;
248
249 $formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
250
251 Hooks::runner()->onUpdateUserMailerFormattedPageStatus( $formattedPageStatus );
252 if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
253 throw new MWException( 'Not a valid page status!' );
254 }
255
256 $userTalkId = false;
257
258 if ( !$minorEdit ||
259 ( $config->get( 'EnotifMinorEdits' ) && !$editor->isAllowed( 'nominornewtalk' ) )
260 ) {
261 if ( $config->get( 'EnotifUserTalk' )
262 && $isUserTalkPage
263 && $this->canSendUserTalkEmail( $editor->getUser(), $title, $minorEdit )
264 ) {
265 $targetUser = User::newFromName( $title->getText() );
266 $this->compose( $targetUser, self::USER_TALK, $messageCache );
267 $userTalkId = $targetUser->getId();
268 }
269
270 if ( $config->get( 'EnotifWatchlist' ) ) {
271 // Send updates to watchers other than the current editor
272 // and don't send to watchers who are blocked and cannot login
273 $userArray = UserArray::newFromIDs( $watchers );
274 foreach ( $userArray as $watchingUser ) {
275 if ( $watchingUser->getOption( 'enotifwatchlistpages' )
276 && ( !$minorEdit || $watchingUser->getOption( 'enotifminoredits' ) )
277 && $watchingUser->isEmailConfirmed()
278 && $watchingUser->getId() != $userTalkId
279 && !in_array( $watchingUser->getName(), $config->get( 'UsersNotifiedOnAllChanges' ) )
280 // @TODO Partial blocks should not prevent the user from logging in.
281 // see: https://phabricator.wikimedia.org/T208895
282 && !( $config->get( 'BlockDisablesLogin' ) && $watchingUser->getBlock() )
283 && Hooks::runner()->onSendWatchlistEmailNotification( $watchingUser, $title, $this )
284 ) {
285 $this->compose( $watchingUser, self::WATCHLIST, $messageCache );
286 }
287 }
288 }
289 }
290
291 foreach ( $config->get( 'UsersNotifiedOnAllChanges' ) as $name ) {
292 if ( $editor->getUser()->getName() == $name ) {
293 // No point notifying the user that actually made the change!
294 continue;
295 }
296 $user = User::newFromName( $name );
297 $this->compose( $user, self::ALL_CHANGES, $messageCache );
298 }
299
300 $this->sendMails();
301 }
302
309 private function canSendUserTalkEmail( UserIdentity $editor, $title, $minorEdit ) {
310 $config = MediaWikiServices::getInstance()->getMainConfig();
311 $isUserTalkPage = ( $title->getNamespace() === NS_USER_TALK );
312
313 if ( !$config->get( 'EnotifUserTalk' ) || !$isUserTalkPage ) {
314 return false;
315 }
316
317 $targetUser = User::newFromName( $title->getText() );
318
319 if ( !$targetUser || $targetUser->isAnon() ) {
320 wfDebug( __METHOD__ . ": user talk page edited, but user does not exist" );
321 } elseif ( $targetUser->getId() == $editor->getId() ) {
322 wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent" );
323 } elseif ( $config->get( 'BlockDisablesLogin' ) && $targetUser->getBlock() ) {
324 // @TODO Partial blocks should not prevent the user from logging in.
325 // see: https://phabricator.wikimedia.org/T208895
326 wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent" );
327 } elseif ( $targetUser->getOption( 'enotifusertalkpages' )
328 && ( !$minorEdit || $targetUser->getOption( 'enotifminoredits' ) )
329 ) {
330 if ( !$targetUser->isEmailConfirmed() ) {
331 wfDebug( __METHOD__ . ": talk page owner doesn't have validated email" );
332 } elseif ( !Hooks::runner()->onAbortTalkPageEmailNotification( $targetUser, $title ) ) {
333 wfDebug( __METHOD__ . ": talk page update notification is aborted for this user" );
334 } else {
335 wfDebug( __METHOD__ . ": sending talk page update notification" );
336 return true;
337 }
338 } else {
339 wfDebug( __METHOD__ . ": talk page owner doesn't want notifications" );
340 }
341 return false;
342 }
343
348 private function composeCommonMailtext( MessageCache $messageCache ) {
349 $config = MediaWikiServices::getInstance()->getMainConfig();
350
351 $this->composed_common = true;
352
353 # You as the WikiAdmin and Sysops can make use of plenty of
354 # named variables when composing your notification emails while
355 # simply editing the Meta pages
356
357 $keys = [];
358 $postTransformKeys = [];
359 $pageTitleUrl = $this->title->getCanonicalURL();
360 $pageTitle = $this->title->getPrefixedText();
361
362 if ( $this->oldid ) {
363 // Always show a link to the diff which triggered the mail. See T34210.
364 $keys['$NEWPAGE'] = "\n\n" . wfMessage(
365 'enotif_lastdiff',
366 $this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] )
367 )->inContentLanguage()->text();
368
369 if ( !$config->get( 'EnotifImpersonal' ) ) {
370 // For personal mail, also show a link to the diff of all changes
371 // since last visited.
372 $keys['$NEWPAGE'] .= "\n\n" . wfMessage(
373 'enotif_lastvisited',
374 $this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] )
375 )->inContentLanguage()->text();
376 }
377 $keys['$OLDID'] = $this->oldid;
378 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
379 $keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
380 } else {
381 # clear $OLDID placeholder in the message template
382 $keys['$OLDID'] = '';
383 $keys['$NEWPAGE'] = '';
384 // Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
385 $keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
386 }
387
388 $keys['$PAGETITLE'] = $this->title->getPrefixedText();
389 $keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
390 $keys['$PAGEMINOREDIT'] = $this->minorEdit ?
391 "\n\n" . wfMessage( 'enotif_minoredit' )->inContentLanguage()->text() :
392 '';
393 $keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
394
395 if ( !$this->editor->isRegistered() ) {
396 # real anon (user:xxx.xxx.xxx.xxx)
397 $keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
398 ->inContentLanguage()->text();
399 $keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
400
401 } else {
402 $keys['$PAGEEDITOR'] = $config->get( 'EnotifUseRealName' ) && $this->editor->getRealName() !== ''
403 ? $this->editor->getRealName() : $this->editor->getName();
404 $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
405 $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
406 }
407
408 $keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
409 $keys['$HELPPAGE'] = wfExpandUrl(
410 Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
411 );
412
413 # Replace this after transforming the message, T37019
414 $postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
415
416 // Now build message's subject and body
417
418 // Messages:
419 // enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
420 // enotif_subject_restored, enotif_subject_changed
421 $this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
422 ->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
423
424 // Messages:
425 // enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
426 // enotif_body_intro_restored, enotif_body_intro_changed
427 $keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
428 ->inContentLanguage()
429 ->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
430 ->text();
431
432 $body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
433 $body = strtr( $body, $keys );
434 $body = $messageCache->transform( $body, false, null, $this->title );
435 $this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
436
437 # Reveal the page editor's address as REPLY-TO address only if
438 # the user has not opted-out and the option is enabled at the
439 # global configuration level.
440 $adminAddress = new MailAddress(
441 $config->get( 'PasswordSender' ),
442 wfMessage( 'emailsender' )->inContentLanguage()->text()
443 );
444 if ( $config->get( 'EnotifRevealEditorAddress' )
445 && ( $this->editor->getEmail() != '' )
446 && $this->editor->getOption( 'enotifrevealaddr' )
447 ) {
448 $editorAddress = MailAddress::newFromUser( $this->editor );
449 if ( $config->get( 'EnotifFromEditor' ) ) {
450 $this->from = $editorAddress;
451 } else {
452 $this->from = $adminAddress;
453 $this->replyto = $editorAddress;
454 }
455 } else {
456 $this->from = $adminAddress;
457 $this->replyto = new MailAddress(
458 $config->get( 'NoReplyAddress' )
459 );
460 }
461 }
462
472 private function compose( UserEmailContact $user, $source, MessageCache $messageCache ) {
473 if ( !$this->composed_common ) {
474 $this->composeCommonMailtext( $messageCache );
475 }
476
477 if ( MediaWikiServices::getInstance()->getMainConfig()->get( 'EnotifImpersonal' ) ) {
478 $this->mailTargets[] = MailAddress::newFromUser( $user );
479 } else {
480 $this->sendPersonalised( $user, $source );
481 }
482 }
483
487 private function sendMails() {
488 if ( MediaWikiServices::getInstance()->getMainConfig()->get( 'EnotifImpersonal' ) ) {
489 $this->sendImpersonal( $this->mailTargets );
490 }
491 }
492
503 private function sendPersonalised( UserEmailContact $watchingUser, $source ) {
504 // From the PHP manual:
505 // Note: The to parameter cannot be an address in the form of
506 // "Something <someone@example.com>". The mail command will not parse
507 // this properly while talking with the MTA.
508 $to = MailAddress::newFromUser( $watchingUser );
509
510 # $PAGEEDITDATE is the time and date of the page change
511 # expressed in terms of individual local time of the notification
512 # recipient, i.e. watching user
513 $mwServices = MediaWikiServices::getInstance();
514 $contLang = $mwServices->getContentLanguage();
515 $watchingUserName = (
516 $mwServices->getMainConfig()->get( 'EnotifUseRealName' ) &&
517 $watchingUser->getRealName() !== ''
518 ) ? $watchingUser->getRealName() : $watchingUser->getUser()->getName();
519 $body = str_replace(
520 [
521 '$WATCHINGUSERNAME',
522 '$PAGEEDITDATE',
523 '$PAGEEDITTIME'
524 ],
525 [
526 $watchingUserName,
527 $contLang->userDate( $this->timestamp, $watchingUser->getUser() ),
528 $contLang->userTime( $this->timestamp, $watchingUser->getUser() )
529 ],
530 $this->body
531 );
532
533 $headers = [];
534 if ( $source === self::WATCHLIST ) {
535 $headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
536 }
537
538 return UserMailer::send( $to, $this->from, $this->subject, $body, [
539 'replyTo' => $this->replyto,
540 'headers' => $headers,
541 ] );
542 }
543
550 private function sendImpersonal( $addresses ) {
551 if ( empty( $addresses ) ) {
552 return null;
553 }
554
555 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
556 $body = str_replace(
557 [
558 '$WATCHINGUSERNAME',
559 '$PAGEEDITDATE',
560 '$PAGEEDITTIME'
561 ],
562 [
563 wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
564 $contLang->date( $this->timestamp, false, false ),
565 $contLang->time( $this->timestamp, false, false )
566 ],
567 $this->body
568 );
569
570 return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
571 'replyTo' => $this->replyto,
572 ] );
573 }
574
575}
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(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
This module processes the email notifications when the current page is changed.
sendPersonalised(UserEmailContact $watchingUser, $source)
Does the per-user customizations to a notification e-mail (name, timestamp in proper timezone,...
sendImpersonal( $addresses)
Same as sendPersonalised but does impersonal mail suitable for bulk mailing.
MailAddress null $replyto
const ALL_CHANGES
Notification because user is notified for all changes.
sendMails()
Send any queued mails.
canSendUserTalkEmail(UserIdentity $editor, $title, $minorEdit)
actuallyNotifyOnPageChange(Authority $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers, $pageStatus='changed')
Immediate version of notifyOnPageChange().
const WATCHLIST
Notification is due to a watchlisted page being edited.
notifyOnPageChange(Authority $editor, $title, $timestamp, $summary, $minorEdit, $oldid=false, $pageStatus='changed')
Send emails corresponding to the user $editor editing the page $title.
const USER_TALK
Notification is due to user's user talk being edited.
composeCommonMailtext(MessageCache $messageCache)
Generate the generic "this page has been changed" e-mail text.
compose(UserEmailContact $user, $source, MessageCache $messageCache)
Compose a mail to a given user and either queue it for sending, or send it now, depending on settings...
getPageStatus()
Extensions that have hooks for UpdateUserMailerFormattedPageStatus (to provide additional pageStatus ...
MailAddress null $from
MailAddress[] $mailTargets
Job for email notification mails.
static singleton( $domain=false)
MediaWiki exception.
Stores a single person's name and email address.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Cache of messages that are defined by MediaWiki namespace pages or by hooks.
transform( $message, $interface=false, $language=null, PageReference $page=null)
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:48
static newFromIDs( $ids)
Definition UserArray.php:52
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
static newFromName( $name, $validate='valid')
Definition User.php:607
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Definition User.php:3033
getUser()
Definition User.php:4285
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