Completed
Branch master (68979b)
by
unknown
26:11
created

SpecialEmailUser   F

Complexity

Total Complexity 47

Size/Duplication

Total Lines 400
Duplicated Lines 3.25 %

Coupling/Cohesion

Components 1
Dependencies 23

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 47
c 1
b 0
f 0
lcom 1
cbo 23
dl 13
loc 400
rs 3.8405

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A doesWrites() 0 3 1
A getDescription() 0 8 2
A getFormFields() 0 49 1
C execute() 0 79 15
B getTarget() 0 24 6
D getPermissionsError() 4 40 9
B userForm() 0 31 1
A uiSubmit() 0 3 1
C submit() 0 82 7
A prefixSearchSubpages() 9 9 2
A getGroupName() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SpecialEmailUser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SpecialEmailUser, and based on these observations, apply Extract Interface, too.

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
Bug introduced by
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
		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
				$cc_subject = $context->msg( 'emailccsubject' )->rawParams(
392
					$target->getName(), $subject )->text();
393
394
				// target and sender are equal, because this is the CC for the sender
395
				Hooks::run( 'EmailUserCC', [ &$from, &$from, &$cc_subject, &$text ] );
396
397
				$ccStatus = UserMailer::send( $from, $from, $cc_subject, $text );
398
				$status->merge( $ccStatus );
399
			}
400
401
			Hooks::run( 'EmailUserComplete', [ $to, $from, $subject, $text ] );
402
403
			return $status;
404
		}
405
	}
406
407
	/**
408
	 * Return an array of subpages beginning with $search that this special page will accept.
409
	 *
410
	 * @param string $search Prefix to search for
411
	 * @param int $limit Maximum number of results to return (usually 10)
412
	 * @param int $offset Number of results to skip (usually 0)
413
	 * @return string[] Matching subpages
414
	 */
415 View Code Duplication
	public function prefixSearchSubpages( $search, $limit, $offset ) {
416
		$user = User::newFromName( $search );
417
		if ( !$user ) {
418
			// No prefix suggestion for invalid user
419
			return [];
420
		}
421
		// Autocomplete subpage as user list - public to allow caching
422
		return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
423
	}
424
425
	protected function getGroupName() {
426
		return 'users';
427
	}
428
}
429