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/specials/SpecialEmailuser.php (1 issue)

Labels
Severity

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
 * Implements Special:Emailuser
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
 * @ingroup SpecialPage
22
 */
23
24
/**
25
 * A special page that allows users to send e-mails to other users
26
 *
27
 * @ingroup SpecialPage
28
 */
29
class SpecialEmailUser extends UnlistedSpecialPage {
30
	protected $mTarget;
31
32
	/**
33
	 * @var User|string $mTargetObj
34
	 */
35
	protected $mTargetObj;
36
37
	public function __construct() {
38
		parent::__construct( 'Emailuser' );
39
	}
40
41
	public function doesWrites() {
42
		return true;
43
	}
44
45
	public function getDescription() {
46
		$target = self::getTarget( $this->mTarget );
47
		if ( !$target instanceof User ) {
48
			return $this->msg( 'emailuser-title-notarget' )->text();
49
		}
50
51
		return $this->msg( 'emailuser-title-target', $target->getName() )->text();
52
	}
53
54
	protected function getFormFields() {
55
		return [
56
			'From' => [
57
				'type' => 'info',
58
				'raw' => 1,
59
				'default' => Linker::link(
60
					$this->getUser()->getUserPage(),
61
					htmlspecialchars( $this->getUser()->getName() )
62
				),
63
				'label-message' => 'emailfrom',
64
				'id' => 'mw-emailuser-sender',
65
			],
66
			'To' => [
67
				'type' => 'info',
68
				'raw' => 1,
69
				'default' => Linker::link(
70
					$this->mTargetObj->getUserPage(),
71
					htmlspecialchars( $this->mTargetObj->getName() )
72
				),
73
				'label-message' => 'emailto',
74
				'id' => 'mw-emailuser-recipient',
75
			],
76
			'Target' => [
77
				'type' => 'hidden',
78
				'default' => $this->mTargetObj->getName(),
79
			],
80
			'Subject' => [
81
				'type' => 'text',
82
				'default' => $this->msg( 'defemailsubject',
83
					$this->getUser()->getName() )->inContentLanguage()->text(),
84
				'label-message' => 'emailsubject',
85
				'maxlength' => 200,
86
				'size' => 60,
87
				'required' => true,
88
			],
89
			'Text' => [
90
				'type' => 'textarea',
91
				'rows' => 20,
92
				'cols' => 80,
93
				'label-message' => 'emailmessage',
94
				'required' => true,
95
			],
96
			'CCMe' => [
97
				'type' => 'check',
98
				'label-message' => 'emailccme',
99
				'default' => $this->getUser()->getBoolOption( 'ccmeonemails' ),
100
			],
101
		];
102
	}
103
104
	public function execute( $par ) {
105
		$out = $this->getOutput();
106
		$out->addModuleStyles( 'mediawiki.special' );
107
108
		$this->mTarget = is_null( $par )
109
			? $this->getRequest()->getVal( 'wpTarget', $this->getRequest()->getVal( 'target', '' ) )
110
			: $par;
111
112
		// This needs to be below assignment of $this->mTarget because
113
		// getDescription() needs it to determine the correct page title.
114
		$this->setHeaders();
115
		$this->outputHeader();
116
117
		// error out if sending user cannot do this
118
		$error = self::getPermissionsError(
119
			$this->getUser(),
120
			$this->getRequest()->getVal( 'wpEditToken' ),
121
			$this->getConfig()
122
		);
123
124
		switch ( $error ) {
125
			case null:
0 ignored issues
show
It seems like you are loosely comparing $error of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
126
				# Wahey!
127
				break;
128
			case 'badaccess':
129
				throw new PermissionsError( 'sendemail' );
130
			case 'blockedemailuser':
131
				throw new UserBlockedError( $this->getUser()->mBlock );
132
			case 'actionthrottledtext':
133
				throw new ThrottledError;
134
			case 'mailnologin':
135
			case 'usermaildisabled':
136
				throw new ErrorPageError( $error, "{$error}text" );
137
			default:
138
				# It's a hook error
139
				list( $title, $msg, $params ) = $error;
140
				throw new ErrorPageError( $title, $msg, $params );
141
		}
142
		// Got a valid target user name? Else ask for one.
143
		$ret = self::getTarget( $this->mTarget );
144
		if ( !$ret instanceof User ) {
145
			if ( $this->mTarget != '' ) {
146
				// Messages used here: notargettext, noemailtext, nowikiemailtext
147
				$ret = ( $ret == 'notarget' ) ? 'emailnotarget' : ( $ret . 'text' );
148
				$out->wrapWikiMsg( "<p class='error'>$1</p>", $ret );
149
			}
150
			$out->addHTML( $this->userForm( $this->mTarget ) );
151
152
			return;
153
		}
154
155
		$this->mTargetObj = $ret;
156
157
		// Set the 'relevant user' in the skin, so it displays links like Contributions,
158
		// User logs, UserRights, etc.
159
		$this->getSkin()->setRelevantUser( $this->mTargetObj );
160
161
		$context = new DerivativeContext( $this->getContext() );
162
		$context->setTitle( $this->getPageTitle() ); // Remove subpage
163
		$form = new HTMLForm( $this->getFormFields(), $context );
164
		// By now we are supposed to be sure that $this->mTarget is a user name
165
		$form->addPreText( $this->msg( 'emailpagetext', $this->mTarget )->parse() );
166
		$form->setSubmitTextMsg( 'emailsend' );
167
		$form->setSubmitCallback( [ __CLASS__, 'uiSubmit' ] );
168
		$form->setWrapperLegendMsg( 'email-legend' );
169
		$form->loadData();
170
171
		if ( !Hooks::run( 'EmailUserForm', [ &$form ] ) ) {
172
			return;
173
		}
174
175
		$result = $form->show();
176
177
		if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
178
			$out->setPageTitle( $this->msg( 'emailsent' ) );
179
			$out->addWikiMsg( 'emailsenttext', $this->mTarget );
180
			$out->returnToMain( false, $this->mTargetObj->getUserPage() );
181
		}
182
	}
183
184
	/**
185
	 * Validate target User
186
	 *
187
	 * @param string $target Target user name
188
	 * @return User User object on success or a string on error
189
	 */
190
	public static function getTarget( $target ) {
191
		if ( $target == '' ) {
192
			wfDebug( "Target is empty.\n" );
193
194
			return 'notarget';
195
		}
196
197
		$nu = User::newFromName( $target );
198
		if ( !$nu instanceof User || !$nu->getId() ) {
199
			wfDebug( "Target is invalid user.\n" );
200
201
			return 'notarget';
202
		} elseif ( !$nu->isEmailConfirmed() ) {
203
			wfDebug( "User has no valid email.\n" );
204
205
			return 'noemail';
206
		} elseif ( !$nu->canReceiveEmail() ) {
207
			wfDebug( "User does not allow user emails.\n" );
208
209
			return 'nowikiemail';
210
		}
211
212
		return $nu;
213
	}
214
215
	/**
216
	 * Check whether a user is allowed to send email
217
	 *
218
	 * @param User $user
219
	 * @param string $editToken Edit token
220
	 * @param Config $config optional for backwards compatibility
221
	 * @return string|null Null on success or string on error
222
	 */
223
	public static function getPermissionsError( $user, $editToken, Config $config = null ) {
224 View Code Duplication
		if ( $config === null ) {
225
			wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
226
			$config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
227
		}
228
		if ( !$config->get( 'EnableEmail' ) || !$config->get( 'EnableUserEmail' ) ) {
229
			return 'usermaildisabled';
230
		}
231
232
		if ( !$user->isAllowed( 'sendemail' ) ) {
233
			return 'badaccess';
234
		}
235
236
		if ( !$user->isEmailConfirmed() ) {
237
			return 'mailnologin';
238
		}
239
240
		if ( $user->isBlockedFromEmailuser() ) {
241
			wfDebug( "User is blocked from sending e-mail.\n" );
242
243
			return "blockedemailuser";
244
		}
245
246
		if ( $user->pingLimiter( 'emailuser' ) ) {
247
			wfDebug( "Ping limiter triggered.\n" );
248
249
			return 'actionthrottledtext';
250
		}
251
252
		$hookErr = false;
253
254
		Hooks::run( 'UserCanSendEmail', [ &$user, &$hookErr ] );
255
		Hooks::run( 'EmailUserPermissionsErrors', [ $user, $editToken, &$hookErr ] );
256
257
		if ( $hookErr ) {
258
			return $hookErr;
259
		}
260
261
		return null;
262
	}
263
264
	/**
265
	 * Form to ask for target user name.
266
	 *
267
	 * @param string $name User name submitted.
268
	 * @return string Form asking for user name.
269
	 */
270
	protected function userForm( $name ) {
271
		$this->getOutput()->addModules( 'mediawiki.userSuggest' );
272
		$string = Html::openElement(
273
				'form',
274
				[ 'method' => 'get', 'action' => wfScript(), 'id' => 'askusername' ]
275
			) .
276
			Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
277
			Html::openElement( 'fieldset' ) .
278
			Html::rawElement( 'legend', null, $this->msg( 'emailtarget' )->parse() ) .
279
			Html::label(
280
				$this->msg( 'emailusername' )->text(),
281
				'emailusertarget'
282
			) . '&#160;' .
283
			Html::input(
284
				'target',
285
				$name,
286
				'text',
287
				[
288
					'id' => 'emailusertarget',
289
					'class' => 'mw-autocomplete-user',  // used by mediawiki.userSuggest
290
					'autofocus' => true,
291
					'size' => 30,
292
				]
293
			) .
294
			' ' .
295
			Html::submitButton( $this->msg( 'emailusernamesubmit' )->text(), [] ) .
296
			Html::closeElement( 'fieldset' ) .
297
			Html::closeElement( 'form' ) . "\n";
298
299
		return $string;
300
	}
301
302
	/**
303
	 * Submit callback for an HTMLForm object, will simply call submit().
304
	 *
305
	 * @since 1.20
306
	 * @param array $data
307
	 * @param HTMLForm $form
308
	 * @return Status|string|bool
309
	 */
310
	public static function uiSubmit( array $data, HTMLForm $form ) {
311
		return self::submit( $data, $form->getContext() );
312
	}
313
314
	/**
315
	 * Really send a mail. Permissions should have been checked using
316
	 * getPermissionsError(). It is probably also a good
317
	 * idea to check the edit token and ping limiter in advance.
318
	 *
319
	 * @param array $data
320
	 * @param IContextSource $context
321
	 * @return Status|string|bool Status object, or potentially a String on error
322
	 * or maybe even true on success if anything uses the EmailUser hook.
323
	 */
324
	public static function submit( array $data, IContextSource $context ) {
325
		$config = $context->getConfig();
326
327
		$target = self::getTarget( $data['Target'] );
328
		if ( !$target instanceof User ) {
329
			// Messages used here: notargettext, noemailtext, nowikiemailtext
330
			return $context->msg( $target . 'text' )->parseAsBlock();
331
		}
332
333
		$to = MailAddress::newFromUser( $target );
334
		$from = MailAddress::newFromUser( $context->getUser() );
335
		$subject = $data['Subject'];
336
		$text = $data['Text'];
337
338
		// Add a standard footer and trim up trailing newlines
339
		$text = rtrim( $text ) . "\n\n-- \n";
340
		$text .= $context->msg( 'emailuserfooter',
341
			$from->name, $to->name )->inContentLanguage()->text();
342
343
		$error = '';
344
		if ( !Hooks::run( 'EmailUser', [ &$to, &$from, &$subject, &$text, &$error ] ) ) {
345
			return $error;
346
		}
347
348 View Code Duplication
		if ( $config->get( 'UserEmailUseReplyTo' ) ) {
349
			/**
350
			 * Put the generic wiki autogenerated address in the From:
351
			 * header and reserve the user for Reply-To.
352
			 *
353
			 * This is a bit ugly, but will serve to differentiate
354
			 * wiki-borne mails from direct mails and protects against
355
			 * SPF and bounce problems with some mailers (see below).
356
			 */
357
			$mailFrom = new MailAddress( $config->get( 'PasswordSender' ),
358
				wfMessage( 'emailsender' )->inContentLanguage()->text() );
359
			$replyTo = $from;
360
		} else {
361
			/**
362
			 * Put the sending user's e-mail address in the From: header.
363
			 *
364
			 * This is clean-looking and convenient, but has issues.
365
			 * One is that it doesn't as clearly differentiate the wiki mail
366
			 * from "directly" sent mails.
367
			 *
368
			 * Another is that some mailers (like sSMTP) will use the From
369
			 * address as the envelope sender as well. For open sites this
370
			 * can cause mails to be flunked for SPF violations (since the
371
			 * wiki server isn't an authorized sender for various users'
372
			 * domains) as well as creating a privacy issue as bounces
373
			 * containing the recipient's e-mail address may get sent to
374
			 * the sending user.
375
			 */
376
			$mailFrom = $from;
377
			$replyTo = null;
378
		}
379
380
		$status = UserMailer::send( $to, $mailFrom, $subject, $text, [
381
			'replyTo' => $replyTo,
382
		] );
383
384
		if ( !$status->isGood() ) {
385
			return $status;
386
		} else {
387
			// if the user requested a copy of this mail, do this now,
388
			// unless they are emailing themselves, in which case one
389
			// copy of the message is sufficient.
390
			if ( $data['CCMe'] && $to != $from ) {
391
				$ccTo = $from;
392
				$ccFrom = $from;
393
				$ccSubject = $context->msg( 'emailccsubject' )->rawParams(
394
					$target->getName(), $subject )->text();
395
				$ccText = $text;
396
397
				Hooks::run( 'EmailUserCC', [ &$ccTo, &$ccFrom, &$ccSubject, &$ccText ] );
398
399 View Code Duplication
				if ( $config->get( 'UserEmailUseReplyTo' ) ) {
400
					$mailFrom = new MailAddress(
401
						$config->get( 'PasswordSender' ),
402
						wfMessage( 'emailsender' )->inContentLanguage()->text()
403
					);
404
					$replyTo = $ccFrom;
405
				} else {
406
					$mailFrom = $ccFrom;
407
					$replyTo = null;
408
				}
409
410
				$ccStatus = UserMailer::send(
411
					$ccTo, $mailFrom, $ccSubject, $ccText, [
412
						'replyTo' => $replyTo,
413
				] );
414
				$status->merge( $ccStatus );
415
			}
416
417
			Hooks::run( 'EmailUserComplete', [ $to, $from, $subject, $text ] );
418
419
			return $status;
420
		}
421
	}
422
423
	/**
424
	 * Return an array of subpages beginning with $search that this special page will accept.
425
	 *
426
	 * @param string $search Prefix to search for
427
	 * @param int $limit Maximum number of results to return (usually 10)
428
	 * @param int $offset Number of results to skip (usually 0)
429
	 * @return string[] Matching subpages
430
	 */
431 View Code Duplication
	public function prefixSearchSubpages( $search, $limit, $offset ) {
432
		$user = User::newFromName( $search );
433
		if ( !$user ) {
434
			// No prefix suggestion for invalid user
435
			return [];
436
		}
437
		// Autocomplete subpage as user list - public to allow caching
438
		return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
439
	}
440
441
	protected function getGroupName() {
442
		return 'users';
443
	}
444
}
445