EmailNotification::notifyOnPageChange()   C
last analyzed

Complexity

Conditions 13
Paths 17

Size

Total Lines 55
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 32
nc 17
nop 7
dl 0
loc 55
rs 6.7747
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Classes used to send e-mails
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @author <[email protected]>
22
 * @author <[email protected]>
23
 * @author Tim Starling
24
 * @author Luke Welling [email protected]
25
 */
26
use MediaWiki\Linker\LinkTarget;
27
28
use MediaWiki\MediaWikiServices;
29
30
/**
31
 * This module processes the email notifications when the current page is
32
 * changed. It looks up the table watchlist to find out which users are watching
33
 * that page.
34
 *
35
 * The current implementation sends independent emails to each watching user for
36
 * the following reason:
37
 *
38
 * - Each watching user will be notified about the page edit time expressed in
39
 * his/her local time (UTC is shown additionally). To achieve this, we need to
40
 * find the individual timeoffset of each watching user from the preferences..
41
 *
42
 * Suggested improvement to slack down the number of sent emails: We could think
43
 * of sending out bulk mails (bcc:user1,user2...) for all these users having the
44
 * same timeoffset in their preferences.
45
 *
46
 * Visit the documentation pages under http://meta.wikipedia.com/Enotif
47
 */
48
class EmailNotification {
49
50
	/**
51
	 * Notification is due to user's user talk being edited
52
	 */
53
	const USER_TALK = 'user_talk';
54
	/**
55
	 * Notification is due to a watchlisted page being edited
56
	 */
57
	const WATCHLIST = 'watchlist';
58
	/**
59
	 * Notification because user is notified for all changes
60
	 */
61
	const ALL_CHANGES = 'all_changes';
62
63
	protected $subject, $body, $replyto, $from;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
64
	protected $timestamp, $summary, $minorEdit, $oldid, $composed_common, $pageStatus;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
65
	protected $mailTargets = [];
66
67
	/**
68
	 * @var Title
69
	 */
70
	protected $title;
71
72
	/**
73
	 * @var User
74
	 */
75
	protected $editor;
76
77
	/**
78
	 * @deprecated since 1.27 use WatchedItemStore::updateNotificationTimestamp directly
79
	 *
80
	 * @param User $editor The editor that triggered the update.  Their notification
81
	 *  timestamp will not be updated(they have already seen it)
82
	 * @param LinkTarget $linkTarget The link target of the title to update timestamps for
83
	 * @param string $timestamp Set the update timestamp to this value
84
	 *
85
	 * @return int[] Array of user IDs
86
	 */
87
	public static function updateWatchlistTimestamp(
88
		User $editor,
89
		LinkTarget $linkTarget,
90
		$timestamp
91
	) {
92
		// wfDeprecated( __METHOD__, '1.27' );
93
		$config = RequestContext::getMain()->getConfig();
94
		if ( !$config->get( 'EnotifWatchlist' ) && !$config->get( 'ShowUpdatedMarker' ) ) {
95
			return [];
96
		}
97
		return MediaWikiServices::getInstance()->getWatchedItemStore()->updateNotificationTimestamp(
98
			$editor,
99
			$linkTarget,
100
			$timestamp
101
		);
102
	}
103
104
	/**
105
	 * Send emails corresponding to the user $editor editing the page $title.
106
	 *
107
	 * May be deferred via the job queue.
108
	 *
109
	 * @param User $editor
110
	 * @param Title $title
111
	 * @param string $timestamp
112
	 * @param string $summary
113
	 * @param bool $minorEdit
114
	 * @param bool $oldid (default: false)
115
	 * @param string $pageStatus (default: 'changed')
116
	 */
117
	public function notifyOnPageChange( $editor, $title, $timestamp, $summary,
118
		$minorEdit, $oldid = false, $pageStatus = 'changed'
119
	) {
120
		global $wgEnotifMinorEdits, $wgUsersNotifiedOnAllChanges, $wgEnotifUserTalk;
121
122
		if ( $title->getNamespace() < 0 ) {
123
			return;
124
		}
125
126
		// update wl_notificationtimestamp for watchers
127
		$config = RequestContext::getMain()->getConfig();
128
		$watchers = [];
129
		if ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) {
130
			$watchers = MediaWikiServices::getInstance()->getWatchedItemStore()->updateNotificationTimestamp(
131
				$editor,
132
				$title,
133
				$timestamp
134
			);
135
		}
136
137
		$sendEmail = true;
138
		// $watchers deals with $wgEnotifWatchlist.
139
		// If nobody is watching the page, and there are no users notified on all changes
140
		// don't bother creating a job/trying to send emails, unless it's a
141
		// talk page with an applicable notification.
142
		if ( !count( $watchers ) && !count( $wgUsersNotifiedOnAllChanges ) ) {
143
			$sendEmail = false;
144
			// Only send notification for non minor edits, unless $wgEnotifMinorEdits
145
			if ( !$minorEdit || ( $wgEnotifMinorEdits && !$editor->isAllowed( 'nominornewtalk' ) ) ) {
146
				$isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
147
				if ( $wgEnotifUserTalk
148
					&& $isUserTalkPage
149
					&& $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
150
				) {
151
					$sendEmail = true;
152
				}
153
			}
154
		}
155
156
		if ( $sendEmail ) {
157
			JobQueueGroup::singleton()->lazyPush( new EnotifNotifyJob(
158
				$title,
159
				[
160
					'editor' => $editor->getName(),
161
					'editorID' => $editor->getId(),
162
					'timestamp' => $timestamp,
163
					'summary' => $summary,
164
					'minorEdit' => $minorEdit,
165
					'oldid' => $oldid,
166
					'watchers' => $watchers,
167
					'pageStatus' => $pageStatus
168
				]
169
			) );
170
		}
171
	}
172
173
	/**
174
	 * Immediate version of notifyOnPageChange().
175
	 *
176
	 * Send emails corresponding to the user $editor editing the page $title.
177
	 *
178
	 * @note Do not call directly. Use notifyOnPageChange so that wl_notificationtimestamp is updated.
179
	 * @param User $editor
180
	 * @param Title $title
181
	 * @param string $timestamp Edit timestamp
182
	 * @param string $summary Edit summary
183
	 * @param bool $minorEdit
184
	 * @param int $oldid Revision ID
185
	 * @param array $watchers Array of user IDs
186
	 * @param string $pageStatus
187
	 * @throws MWException
188
	 */
189
	public function actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit,
190
		$oldid, $watchers, $pageStatus = 'changed' ) {
191
		# we use $wgPasswordSender as sender's address
192
		global $wgUsersNotifiedOnAllChanges;
193
		global $wgEnotifWatchlist, $wgBlockDisablesLogin;
194
		global $wgEnotifMinorEdits, $wgEnotifUserTalk;
195
196
		# The following code is only run, if several conditions are met:
197
		# 1. EmailNotification for pages (other than user_talk pages) must be enabled
198
		# 2. minor edits (changes) are only regarded if the global flag indicates so
199
200
		$isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
201
202
		$this->title = $title;
203
		$this->timestamp = $timestamp;
204
		$this->summary = $summary;
205
		$this->minorEdit = $minorEdit;
206
		$this->oldid = $oldid;
207
		$this->editor = $editor;
208
		$this->composed_common = false;
209
		$this->pageStatus = $pageStatus;
210
211
		$formattedPageStatus = [ 'deleted', 'created', 'moved', 'restored', 'changed' ];
212
213
		Hooks::run( 'UpdateUserMailerFormattedPageStatus', [ &$formattedPageStatus ] );
214
		if ( !in_array( $this->pageStatus, $formattedPageStatus ) ) {
215
			throw new MWException( 'Not a valid page status!' );
216
		}
217
218
		$userTalkId = false;
219
220
		if ( !$minorEdit || ( $wgEnotifMinorEdits && !$editor->isAllowed( 'nominornewtalk' ) ) ) {
221
			if ( $wgEnotifUserTalk
222
				&& $isUserTalkPage
223
				&& $this->canSendUserTalkEmail( $editor, $title, $minorEdit )
224
			) {
225
				$targetUser = User::newFromName( $title->getText() );
226
				$this->compose( $targetUser, self::USER_TALK );
0 ignored issues
show
Security Bug introduced by
It seems like $targetUser defined by \User::newFromName($title->getText()) on line 225 can also be of type false; however, EmailNotification::compose() does only seem to accept object<User>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
227
				$userTalkId = $targetUser->getId();
228
			}
229
230
			if ( $wgEnotifWatchlist ) {
231
				// Send updates to watchers other than the current editor
232
				// and don't send to watchers who are blocked and cannot login
233
				$userArray = UserArray::newFromIDs( $watchers );
234
				foreach ( $userArray as $watchingUser ) {
0 ignored issues
show
Bug introduced by
The expression $userArray of type object<UserArrayFromResult>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
235
					if ( $watchingUser->getOption( 'enotifwatchlistpages' )
236
						&& ( !$minorEdit || $watchingUser->getOption( 'enotifminoredits' ) )
237
						&& $watchingUser->isEmailConfirmed()
238
						&& $watchingUser->getId() != $userTalkId
239
						&& !in_array( $watchingUser->getName(), $wgUsersNotifiedOnAllChanges )
240
						&& !( $wgBlockDisablesLogin && $watchingUser->isBlocked() )
241
					) {
242
						if ( Hooks::run( 'SendWatchlistEmailNotification', [ $watchingUser, $title, $this ] ) ) {
243
							$this->compose( $watchingUser, self::WATCHLIST );
244
						}
245
					}
246
				}
247
			}
248
		}
249
250
		foreach ( $wgUsersNotifiedOnAllChanges as $name ) {
251
			if ( $editor->getName() == $name ) {
252
				// No point notifying the user that actually made the change!
253
				continue;
254
			}
255
			$user = User::newFromName( $name );
256
			$this->compose( $user, self::ALL_CHANGES );
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \User::newFromName($name) on line 255 can also be of type false; however, EmailNotification::compose() does only seem to accept object<User>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
257
		}
258
259
		$this->sendMails();
260
	}
261
262
	/**
263
	 * @param User $editor
264
	 * @param Title $title
265
	 * @param bool $minorEdit
266
	 * @return bool
267
	 */
268
	private function canSendUserTalkEmail( $editor, $title, $minorEdit ) {
269
		global $wgEnotifUserTalk, $wgBlockDisablesLogin;
270
		$isUserTalkPage = ( $title->getNamespace() == NS_USER_TALK );
271
272
		if ( $wgEnotifUserTalk && $isUserTalkPage ) {
273
			$targetUser = User::newFromName( $title->getText() );
274
275
			if ( !$targetUser || $targetUser->isAnon() ) {
276
				wfDebug( __METHOD__ . ": user talk page edited, but user does not exist\n" );
277
			} elseif ( $targetUser->getId() == $editor->getId() ) {
278
				wfDebug( __METHOD__ . ": user edited their own talk page, no notification sent\n" );
279
			} elseif ( $wgBlockDisablesLogin && $targetUser->isBlocked() ) {
280
				wfDebug( __METHOD__ . ": talk page owner is blocked and cannot login, no notification sent\n" );
281
			} elseif ( $targetUser->getOption( 'enotifusertalkpages' )
282
				&& ( !$minorEdit || $targetUser->getOption( 'enotifminoredits' ) )
283
			) {
284
				if ( !$targetUser->isEmailConfirmed() ) {
285
					wfDebug( __METHOD__ . ": talk page owner doesn't have validated email\n" );
286
				} elseif ( !Hooks::run( 'AbortTalkPageEmailNotification', [ $targetUser, $title ] ) ) {
287
					wfDebug( __METHOD__ . ": talk page update notification is aborted for this user\n" );
288
				} else {
289
					wfDebug( __METHOD__ . ": sending talk page update notification\n" );
290
					return true;
291
				}
292
			} else {
293
				wfDebug( __METHOD__ . ": talk page owner doesn't want notifications\n" );
294
			}
295
		}
296
		return false;
297
	}
298
299
	/**
300
	 * Generate the generic "this page has been changed" e-mail text.
301
	 */
302
	private function composeCommonMailtext() {
303
		global $wgPasswordSender, $wgNoReplyAddress;
304
		global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress;
305
		global $wgEnotifImpersonal, $wgEnotifUseRealName;
306
307
		$this->composed_common = true;
308
309
		# You as the WikiAdmin and Sysops can make use of plenty of
310
		# named variables when composing your notification emails while
311
		# simply editing the Meta pages
312
313
		$keys = [];
314
		$postTransformKeys = [];
315
		$pageTitleUrl = $this->title->getCanonicalURL();
316
		$pageTitle = $this->title->getPrefixedText();
317
318
		if ( $this->oldid ) {
319
			// Always show a link to the diff which triggered the mail. See bug 32210.
320
			$keys['$NEWPAGE'] = "\n\n" . wfMessage( 'enotif_lastdiff',
321
					$this->title->getCanonicalURL( [ 'diff' => 'next', 'oldid' => $this->oldid ] ) )
322
					->inContentLanguage()->text();
323
324
			if ( !$wgEnotifImpersonal ) {
325
				// For personal mail, also show a link to the diff of all changes
326
				// since last visited.
327
				$keys['$NEWPAGE'] .= "\n\n" . wfMessage( 'enotif_lastvisited',
328
						$this->title->getCanonicalURL( [ 'diff' => '0', 'oldid' => $this->oldid ] ) )
329
						->inContentLanguage()->text();
330
			}
331
			$keys['$OLDID'] = $this->oldid;
332
			// Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
333
			$keys['$CHANGEDORCREATED'] = wfMessage( 'changed' )->inContentLanguage()->text();
334
		} else {
335
			# clear $OLDID placeholder in the message template
336
			$keys['$OLDID'] = '';
337
			$keys['$NEWPAGE'] = '';
338
			// Deprecated since MediaWiki 1.21, not used by default. Kept for backwards-compatibility.
339
			$keys['$CHANGEDORCREATED'] = wfMessage( 'created' )->inContentLanguage()->text();
340
		}
341
342
		$keys['$PAGETITLE'] = $this->title->getPrefixedText();
343
		$keys['$PAGETITLE_URL'] = $this->title->getCanonicalURL();
344
		$keys['$PAGEMINOREDIT'] = $this->minorEdit ?
345
			wfMessage( 'minoredit' )->inContentLanguage()->text() : '';
346
		$keys['$UNWATCHURL'] = $this->title->getCanonicalURL( 'action=unwatch' );
347
348
		if ( $this->editor->isAnon() ) {
349
			# real anon (user:xxx.xxx.xxx.xxx)
350
			$keys['$PAGEEDITOR'] = wfMessage( 'enotif_anon_editor', $this->editor->getName() )
351
				->inContentLanguage()->text();
352
			$keys['$PAGEEDITOR_EMAIL'] = wfMessage( 'noemailtitle' )->inContentLanguage()->text();
353
354
		} else {
355
			$keys['$PAGEEDITOR'] = $wgEnotifUseRealName && $this->editor->getRealName() !== ''
356
				? $this->editor->getRealName() : $this->editor->getName();
357
			$emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $this->editor->getName() );
358
			$keys['$PAGEEDITOR_EMAIL'] = $emailPage->getCanonicalURL();
359
		}
360
361
		$keys['$PAGEEDITOR_WIKI'] = $this->editor->getUserPage()->getCanonicalURL();
362
		$keys['$HELPPAGE'] = wfExpandUrl(
363
			Skin::makeInternalOrExternalUrl( wfMessage( 'helppage' )->inContentLanguage()->text() )
364
		);
365
366
		# Replace this after transforming the message, bug 35019
367
		$postTransformKeys['$PAGESUMMARY'] = $this->summary == '' ? ' - ' : $this->summary;
368
369
		// Now build message's subject and body
370
371
		// Messages:
372
		// enotif_subject_deleted, enotif_subject_created, enotif_subject_moved,
373
		// enotif_subject_restored, enotif_subject_changed
374
		$this->subject = wfMessage( 'enotif_subject_' . $this->pageStatus )->inContentLanguage()
375
			->params( $pageTitle, $keys['$PAGEEDITOR'] )->text();
376
377
		// Messages:
378
		// enotif_body_intro_deleted, enotif_body_intro_created, enotif_body_intro_moved,
379
		// enotif_body_intro_restored, enotif_body_intro_changed
380
		$keys['$PAGEINTRO'] = wfMessage( 'enotif_body_intro_' . $this->pageStatus )
381
			->inContentLanguage()->params( $pageTitle, $keys['$PAGEEDITOR'], $pageTitleUrl )
382
			->text();
383
384
		$body = wfMessage( 'enotif_body' )->inContentLanguage()->plain();
385
		$body = strtr( $body, $keys );
386
		$body = MessageCache::singleton()->transform( $body, false, null, $this->title );
387
		$this->body = wordwrap( strtr( $body, $postTransformKeys ), 72 );
388
389
		# Reveal the page editor's address as REPLY-TO address only if
390
		# the user has not opted-out and the option is enabled at the
391
		# global configuration level.
392
		$adminAddress = new MailAddress( $wgPasswordSender,
393
			wfMessage( 'emailsender' )->inContentLanguage()->text() );
394
		if ( $wgEnotifRevealEditorAddress
395
			&& ( $this->editor->getEmail() != '' )
396
			&& $this->editor->getOption( 'enotifrevealaddr' )
397
		) {
398
			$editorAddress = MailAddress::newFromUser( $this->editor );
399
			if ( $wgEnotifFromEditor ) {
400
				$this->from = $editorAddress;
401
			} else {
402
				$this->from = $adminAddress;
403
				$this->replyto = $editorAddress;
404
			}
405
		} else {
406
			$this->from = $adminAddress;
407
			$this->replyto = new MailAddress( $wgNoReplyAddress );
408
		}
409
	}
410
411
	/**
412
	 * Compose a mail to a given user and either queue it for sending, or send it now,
413
	 * depending on settings.
414
	 *
415
	 * Call sendMails() to send any mails that were queued.
416
	 * @param User $user
417
	 * @param string $source
418
	 */
419
	function compose( $user, $source ) {
420
		global $wgEnotifImpersonal;
421
422
		if ( !$this->composed_common ) {
423
			$this->composeCommonMailtext();
424
		}
425
426
		if ( $wgEnotifImpersonal ) {
427
			$this->mailTargets[] = MailAddress::newFromUser( $user );
428
		} else {
429
			$this->sendPersonalised( $user, $source );
430
		}
431
	}
432
433
	/**
434
	 * Send any queued mails
435
	 */
436
	function sendMails() {
437
		global $wgEnotifImpersonal;
438
		if ( $wgEnotifImpersonal ) {
439
			$this->sendImpersonal( $this->mailTargets );
440
		}
441
	}
442
443
	/**
444
	 * Does the per-user customizations to a notification e-mail (name,
445
	 * timestamp in proper timezone, etc) and sends it out.
446
	 * Returns true if the mail was sent successfully.
447
	 *
448
	 * @param User $watchingUser
449
	 * @param string $source
450
	 * @return bool
451
	 * @private
452
	 */
453
	function sendPersonalised( $watchingUser, $source ) {
454
		global $wgContLang, $wgEnotifUseRealName;
455
		// From the PHP manual:
456
		//   Note: The to parameter cannot be an address in the form of
457
		//   "Something <[email protected]>". The mail command will not parse
458
		//   this properly while talking with the MTA.
459
		$to = MailAddress::newFromUser( $watchingUser );
460
461
		# $PAGEEDITDATE is the time and date of the page change
462
		# expressed in terms of individual local time of the notification
463
		# recipient, i.e. watching user
464
		$body = str_replace(
465
			[ '$WATCHINGUSERNAME',
466
				'$PAGEEDITDATE',
467
				'$PAGEEDITTIME' ],
468
			[ $wgEnotifUseRealName && $watchingUser->getRealName() !== ''
469
				? $watchingUser->getRealName() : $watchingUser->getName(),
470
				$wgContLang->userDate( $this->timestamp, $watchingUser ),
471
				$wgContLang->userTime( $this->timestamp, $watchingUser ) ],
472
			$this->body );
473
474
		$headers = [];
475
		if ( $source === self::WATCHLIST ) {
476
			$headers['List-Help'] = 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Watchlist';
477
		}
478
479
		return UserMailer::send( $to, $this->from, $this->subject, $body, [
480
			'replyTo' => $this->replyto,
481
			'headers' => $headers,
482
		] );
483
	}
484
485
	/**
486
	 * Same as sendPersonalised but does impersonal mail suitable for bulk
487
	 * mailing.  Takes an array of MailAddress objects.
488
	 * @param MailAddress[] $addresses
489
	 * @return Status|null
490
	 */
491
	function sendImpersonal( $addresses ) {
492
		global $wgContLang;
493
494
		if ( empty( $addresses ) ) {
495
			return null;
496
		}
497
498
		$body = str_replace(
499
			[ '$WATCHINGUSERNAME',
500
				'$PAGEEDITDATE',
501
				'$PAGEEDITTIME' ],
502
			[ wfMessage( 'enotif_impersonal_salutation' )->inContentLanguage()->text(),
503
				$wgContLang->date( $this->timestamp, false, false ),
504
				$wgContLang->time( $this->timestamp, false, false ) ],
505
			$this->body );
506
507
		return UserMailer::send( $addresses, $this->from, $this->subject, $body, [
508
			'replyTo' => $this->replyto,
509
		] );
510
	}
511
512
}
513