Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/mail/EmailNotification.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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;
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 );
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 ) {
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 );
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