Completed
Branch master (5cbada)
by
unknown
28:59
created

UserrightsPage   F

Complexity

Total Complexity 114

Size/Duplication

Total Lines 771
Duplicated Lines 7.91 %

Coupling/Cohesion

Components 1
Dependencies 23

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 61
loc 771
rs 1.0434
wmc 114
lcom 1
cbo 23

26 Methods

Rating   Name   Duplication   Size   Complexity  
C doSaveUserGroups() 16 57 10
A __construct() 0 3 1
A doesWrites() 0 3 1
A isRestricted() 0 3 1
A userCanExecute() 0 3 1
B userCanChangeRights() 0 12 7
F execute() 5 128 27
A getSuccessURL() 0 3 1
A saveUserGroups() 0 19 3
A addLogEntry() 0 12 1
A editUserGroupsForm() 0 18 2
C fetchUser() 0 58 13
A makeGroupNameList() 0 7 2
B switchForm() 0 38 3
A splitGroups() 0 14 3
B showEditUserGroupsForm() 14 106 6
A buildGroupLink() 0 3 1
A buildGroupMemberLink() 0 3 1
A getAllGroups() 0 3 1
F groupCheckboxes() 0 75 18
A canRemove() 9 9 3
A canAdd() 8 8 3
A changeableGroups() 0 3 1
A showLogFragment() 0 5 1
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 UserrightsPage 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 UserrightsPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Implements Special:Userrights
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
 * Special page to allow managing user group membership
26
 *
27
 * @ingroup SpecialPage
28
 */
29
class UserrightsPage extends SpecialPage {
30
	# The target of the local right-adjuster's interest.  Can be gotten from
31
	# either a GET parameter or a subpage-style parameter, so have a member
32
	# variable for it.
33
	protected $mTarget;
34
	/*
35
	 * @var null|User $mFetchedUser The user object of the target username or null.
36
	 */
37
	protected $mFetchedUser = null;
38
	protected $isself = false;
39
40
	public function __construct() {
41
		parent::__construct( 'Userrights' );
42
	}
43
44
	public function doesWrites() {
45
		return true;
46
	}
47
48
	public function isRestricted() {
49
		return true;
50
	}
51
52
	public function userCanExecute( User $user ) {
53
		return $this->userCanChangeRights( $user, false );
54
	}
55
56
	/**
57
	 * @param User $user
58
	 * @param bool $checkIfSelf
59
	 * @return bool
60
	 */
61
	public function userCanChangeRights( $user, $checkIfSelf = true ) {
62
		$available = $this->changeableGroups();
63
		if ( $user->getId() == 0 ) {
64
			return false;
65
		}
66
67
		return !empty( $available['add'] )
68
			|| !empty( $available['remove'] )
69
			|| ( ( $this->isself || !$checkIfSelf ) &&
70
				( !empty( $available['add-self'] )
71
					|| !empty( $available['remove-self'] ) ) );
72
	}
73
74
	/**
75
	 * Manage forms to be shown according to posted data.
76
	 * Depending on the submit button used, call a form or a save function.
77
	 *
78
	 * @param string|null $par String if any subpage provided, else null
79
	 * @throws UserBlockedError|PermissionsError
80
	 */
81
	public function execute( $par ) {
82
		// If the visitor doesn't have permissions to assign or remove
83
		// any groups, it's a bit silly to give them the user search prompt.
84
85
		$user = $this->getUser();
86
		$request = $this->getRequest();
87
		$out = $this->getOutput();
88
89
		/*
90
		 * If the user is blocked and they only have "partial" access
91
		 * (e.g. they don't have the userrights permission), then don't
92
		 * allow them to use Special:UserRights.
93
		 */
94
		if ( $user->isBlocked() && !$user->isAllowed( 'userrights' ) ) {
95
			throw new UserBlockedError( $user->getBlock() );
0 ignored issues
show
Bug introduced by
It seems like $user->getBlock() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
96
		}
97
98 View Code Duplication
		if ( $par !== null ) {
99
			$this->mTarget = $par;
100
		} else {
101
			$this->mTarget = $request->getVal( 'user' );
102
		}
103
104
		$available = $this->changeableGroups();
105
106
		if ( $this->mTarget === null ) {
107
			/*
108
			 * If the user specified no target, and they can only
109
			 * edit their own groups, automatically set them as the
110
			 * target.
111
			 */
112
			if ( !count( $available['add'] ) && !count( $available['remove'] ) ) {
113
				$this->mTarget = $user->getName();
114
			}
115
		}
116
117
		if ( $this->mTarget !== null && User::getCanonicalName( $this->mTarget ) === $user->getName() ) {
118
			$this->isself = true;
119
		}
120
121
		$fetchedStatus = $this->fetchUser( $this->mTarget );
122
		if ( $fetchedStatus->isOK() ) {
123
			$this->mFetchedUser = $fetchedStatus->value;
124
			if ( $this->mFetchedUser instanceof User ) {
125
				// Set the 'relevant user' in the skin, so it displays links like Contributions,
126
				// User logs, UserRights, etc.
127
				$this->getSkin()->setRelevantUser( $this->mFetchedUser );
128
			}
129
		}
130
131
		if ( !$this->userCanChangeRights( $user, true ) ) {
132
			if ( $this->isself && $request->getCheck( 'success' ) ) {
133
				// bug 48609: if the user just removed its own rights, this would
134
				// leads it in a "permissions error" page. In that case, show a
135
				// message that it can't anymore use this page instead of an error
136
				$this->setHeaders();
137
				$out->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>", 'userrights-removed-self' );
138
				$out->returnToMain();
139
140
				return;
141
			}
142
143
			// @todo FIXME: There may be intermediate groups we can mention.
144
			$msg = $user->isAnon() ? 'userrights-nologin' : 'userrights-notallowed';
145
			throw new PermissionsError( null, [ [ $msg ] ] );
146
		}
147
148
		// show a successbox, if the user rights was saved successfully
149
		if ( $request->getCheck( 'success' ) && $this->mFetchedUser !== null ) {
150
			$out->wrapWikiMsg(
151
				"<div class=\"successbox\">\n$1\n</div>",
152
				[ 'savedrights', $this->mFetchedUser->getName() ]
153
			);
154
		}
155
156
		$this->checkReadOnly();
157
158
		$this->setHeaders();
159
		$this->outputHeader();
160
161
		$out->addModuleStyles( 'mediawiki.special' );
162
		$this->addHelpLink( 'Help:Assigning permissions' );
163
164
		// show the general form
165
		if ( count( $available['add'] ) || count( $available['remove'] ) ) {
166
			$this->switchForm();
167
		}
168
169
		if (
170
			$request->wasPosted() &&
171
			$request->getCheck( 'saveusergroups' ) &&
172
			$this->mTarget !== null &&
173
			$user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
174
		) {
175
			// save settings
176
			if ( !$fetchedStatus->isOK() ) {
177
				$this->getOutput()->addWikiText( $fetchedStatus->getWikiText() );
178
179
				return;
180
			}
181
182
			$targetUser = $this->mFetchedUser;
183
			if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (bug 61252)
184
				$targetUser->clearInstanceCache(); // bug 38989
185
			}
186
187
			if ( $request->getVal( 'conflictcheck-originalgroups' )
188
				!== implode( ',', $targetUser->getGroups() )
189
			) {
190
				$out->addWikiMsg( 'userrights-conflict' );
191
			} else {
192
				$this->saveUserGroups(
193
					$this->mTarget,
194
					$request->getVal( 'user-reason' ),
195
					$targetUser
196
				);
197
198
				$out->redirect( $this->getSuccessURL() );
199
200
				return;
201
			}
202
		}
203
204
		// show some more forms
205
		if ( $this->mTarget !== null ) {
206
			$this->editUserGroupsForm( $this->mTarget );
207
		}
208
	}
209
210
	function getSuccessURL() {
211
		return $this->getPageTitle( $this->mTarget )->getFullURL( [ 'success' => 1 ] );
212
	}
213
214
	/**
215
	 * Save user groups changes in the database.
216
	 * Data comes from the editUserGroupsForm() form function
217
	 *
218
	 * @param string $username Username to apply changes to.
219
	 * @param string $reason Reason for group change
220
	 * @param User|UserRightsProxy $user Target user object.
221
	 * @return null
222
	 */
223
	function saveUserGroups( $username, $reason, $user ) {
224
		$allgroups = $this->getAllGroups();
225
		$addgroup = [];
226
		$removegroup = [];
227
228
		// This could possibly create a highly unlikely race condition if permissions are changed between
229
		//  when the form is loaded and when the form is saved. Ignoring it for the moment.
230
		foreach ( $allgroups as $group ) {
231
			// We'll tell it to remove all unchecked groups, and add all checked groups.
232
			// Later on, this gets filtered for what can actually be removed
233
			if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
234
				$addgroup[] = $group;
235
			} else {
236
				$removegroup[] = $group;
237
			}
238
		}
239
240
		$this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason );
241
	}
242
243
	/**
244
	 * Save user groups changes in the database.
245
	 *
246
	 * @param User|UserRightsProxy $user
247
	 * @param array $add Array of groups to add
248
	 * @param array $remove Array of groups to remove
249
	 * @param string $reason Reason for group change
250
	 * @return array Tuple of added, then removed groups
251
	 */
252
	function doSaveUserGroups( $user, $add, $remove, $reason = '' ) {
253
		// Validate input set...
254
		$isself = $user->getName() == $this->getUser()->getName();
255
		$groups = $user->getGroups();
256
		$changeable = $this->changeableGroups();
257
		$addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
258
		$removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
259
260
		$remove = array_unique(
261
			array_intersect( (array)$remove, $removable, $groups ) );
262
		$add = array_unique( array_diff(
263
			array_intersect( (array)$add, $addable ),
264
			$groups )
265
		);
266
267
		$oldGroups = $user->getGroups();
268
		$newGroups = $oldGroups;
269
270
		// Remove then add groups
271 View Code Duplication
		if ( $remove ) {
272
			foreach ( $remove as $index => $group ) {
273
				if ( !$user->removeGroup( $group ) ) {
274
					unset( $remove[$index] );
275
				}
276
			}
277
			$newGroups = array_diff( $newGroups, $remove );
278
		}
279 View Code Duplication
		if ( $add ) {
280
			foreach ( $add as $index => $group ) {
281
				if ( !$user->addGroup( $group ) ) {
282
					unset( $add[$index] );
283
				}
284
			}
285
			$newGroups = array_merge( $newGroups, $add );
286
		}
287
		$newGroups = array_unique( $newGroups );
288
289
		// Ensure that caches are cleared
290
		$user->invalidateCache();
291
292
		// update groups in external authentication database
293
		Hooks::run( 'UserGroupsChanged', [ $user, $add, $remove, $this->getUser(), $reason ] );
294
		MediaWiki\Auth\AuthManager::callLegacyAuthPlugin(
0 ignored issues
show
Deprecated Code introduced by
The method MediaWiki\Auth\AuthManager::callLegacyAuthPlugin() has been deprecated with message: For backwards compatibility only, should be avoided in new code

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
295
			'updateExternalDBGroups', [ $user, $add, $remove ]
296
		);
297
298
		wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" );
299
		wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" );
300
		// Deprecated in favor of UserGroupsChanged hook
301
		Hooks::run( 'UserRights', [ &$user, $add, $remove ], '1.26' );
302
303
		if ( $newGroups != $oldGroups ) {
304
			$this->addLogEntry( $user, $oldGroups, $newGroups, $reason );
0 ignored issues
show
Bug introduced by
It seems like $user defined by parameter $user on line 252 can also be of type object<UserRightsProxy>; however, UserrightsPage::addLogEntry() does only seem to accept object<User>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
305
		}
306
307
		return [ $add, $remove ];
308
	}
309
310
	/**
311
	 * Add a rights log entry for an action.
312
	 * @param User $user
313
	 * @param array $oldGroups
314
	 * @param array $newGroups
315
	 * @param array $reason
316
	 */
317
	function addLogEntry( $user, $oldGroups, $newGroups, $reason ) {
318
		$logEntry = new ManualLogEntry( 'rights', 'rights' );
319
		$logEntry->setPerformer( $this->getUser() );
320
		$logEntry->setTarget( $user->getUserPage() );
321
		$logEntry->setComment( $reason );
322
		$logEntry->setParameters( [
323
			'4::oldgroups' => $oldGroups,
324
			'5::newgroups' => $newGroups,
325
		] );
326
		$logid = $logEntry->insert();
327
		$logEntry->publish( $logid );
328
	}
329
330
	/**
331
	 * Edit user groups membership
332
	 * @param string $username Name of the user.
333
	 */
334
	function editUserGroupsForm( $username ) {
335
		$status = $this->fetchUser( $username );
336
		if ( !$status->isOK() ) {
337
			$this->getOutput()->addWikiText( $status->getWikiText() );
338
339
			return;
340
		} else {
341
			$user = $status->value;
342
		}
343
344
		$groups = $user->getGroups();
345
346
		$this->showEditUserGroupsForm( $user, $groups );
347
348
		// This isn't really ideal logging behavior, but let's not hide the
349
		// interwiki logs if we're using them as is.
350
		$this->showLogFragment( $user, $this->getOutput() );
351
	}
352
353
	/**
354
	 * Normalize the input username, which may be local or remote, and
355
	 * return a user (or proxy) object for manipulating it.
356
	 *
357
	 * Side effects: error output for invalid access
358
	 * @param string $username
359
	 * @return Status
360
	 */
361
	public function fetchUser( $username ) {
362
		$parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username );
363
		if ( count( $parts ) < 2 ) {
364
			$name = trim( $username );
365
			$database = '';
366
		} else {
367
			list( $name, $database ) = array_map( 'trim', $parts );
368
369
			if ( $database == wfWikiID() ) {
370
				$database = '';
371
			} else {
372
				if ( !$this->getUser()->isAllowed( 'userrights-interwiki' ) ) {
373
					return Status::newFatal( 'userrights-no-interwiki' );
374
				}
375
				if ( !UserRightsProxy::validDatabase( $database ) ) {
376
					return Status::newFatal( 'userrights-nodatabase', $database );
377
				}
378
			}
379
		}
380
381
		if ( $name === '' ) {
382
			return Status::newFatal( 'nouserspecified' );
383
		}
384
385
		if ( $name[0] == '#' ) {
386
			// Numeric ID can be specified...
387
			// We'll do a lookup for the name internally.
388
			$id = intval( substr( $name, 1 ) );
389
390
			if ( $database == '' ) {
391
				$name = User::whoIs( $id );
392
			} else {
393
				$name = UserRightsProxy::whoIs( $database, $id );
394
			}
395
396
			if ( !$name ) {
397
				return Status::newFatal( 'noname' );
398
			}
399
		} else {
400
			$name = User::getCanonicalName( $name );
401
			if ( $name === false ) {
402
				// invalid name
403
				return Status::newFatal( 'nosuchusershort', $username );
404
			}
405
		}
406
407
		if ( $database == '' ) {
408
			$user = User::newFromName( $name );
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type boolean; however, User::newFromName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
409
		} else {
410
			$user = UserRightsProxy::newFromName( $database, $name );
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type boolean; however, UserRightsProxy::newFromName() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
411
		}
412
413
		if ( !$user || $user->isAnon() ) {
414
			return Status::newFatal( 'nosuchusershort', $username );
415
		}
416
417
		return Status::newGood( $user );
418
	}
419
420
	/**
421
	 * @since 1.15
422
	 *
423
	 * @param array $ids
424
	 *
425
	 * @return string
426
	 */
427
	public function makeGroupNameList( $ids ) {
428
		if ( empty( $ids ) ) {
429
			return $this->msg( 'rightsnone' )->inContentLanguage()->text();
430
		} else {
431
			return implode( ', ', $ids );
432
		}
433
	}
434
435
	/**
436
	 * Output a form to allow searching for a user
437
	 */
438
	function switchForm() {
439
		$this->getOutput()->addModules( 'mediawiki.userSuggest' );
440
441
		$this->getOutput()->addHTML(
442
			Html::openElement(
443
				'form',
444
				[
445
					'method' => 'get',
446
					'action' => wfScript(),
447
					'name' => 'uluser',
448
					'id' => 'mw-userrights-form1'
449
				]
450
			) .
451
			Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
452
			Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
453
			Xml::inputLabel(
454
				$this->msg( 'userrights-user-editname' )->text(),
455
				'user',
456
				'username',
457
				30,
458
				str_replace( '_', ' ', $this->mTarget ),
459
				[
460
					'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
461
				] + (
462
					// Set autofocus on blank input and error input
463
					$this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
464
				)
465
			) . ' ' .
466
			Xml::submitButton(
467
				$this->msg(
468
					'editusergroup',
469
					$this->mFetchedUser === null ? '[]' : $this->mFetchedUser->getName()
470
				)->text()
471
			) .
472
			Html::closeElement( 'fieldset' ) .
473
			Html::closeElement( 'form' ) . "\n"
474
		);
475
	}
476
477
	/**
478
	 * Go through used and available groups and return the ones that this
479
	 * form will be able to manipulate based on the current user's system
480
	 * permissions.
481
	 *
482
	 * @param array $groups List of groups the given user is in
483
	 * @return array Tuple of addable, then removable groups
484
	 */
485
	protected function splitGroups( $groups ) {
486
		list( $addable, $removable, $addself, $removeself ) = array_values( $this->changeableGroups() );
487
488
		$removable = array_intersect(
489
			array_merge( $this->isself ? $removeself : [], $removable ),
490
			$groups
491
		); // Can't remove groups the user doesn't have
492
		$addable = array_diff(
493
			array_merge( $this->isself ? $addself : [], $addable ),
494
			$groups
495
		); // Can't add groups the user does have
496
497
		return [ $addable, $removable ];
498
	}
499
500
	/**
501
	 * Show the form to edit group memberships.
502
	 *
503
	 * @param User|UserRightsProxy $user User or UserRightsProxy you're editing
504
	 * @param array $groups Array of groups the user is in
505
	 */
506
	protected function showEditUserGroupsForm( $user, $groups ) {
507
		$list = [];
508
		$membersList = [];
509
		foreach ( $groups as $group ) {
510
			$list[] = self::buildGroupLink( $group );
511
			$membersList[] = self::buildGroupMemberLink( $group );
512
		}
513
514
		$autoList = [];
515
		$autoMembersList = [];
516
		if ( $user instanceof User ) {
517
			foreach ( Autopromote::getAutopromoteGroups( $user ) as $group ) {
518
				$autoList[] = self::buildGroupLink( $group );
519
				$autoMembersList[] = self::buildGroupMemberLink( $group );
520
			}
521
		}
522
523
		$language = $this->getLanguage();
524
		$displayedList = $this->msg( 'userrights-groupsmember-type' )
525
			->rawParams(
526
				$language->listToText( $list ),
527
				$language->listToText( $membersList )
528
			)->escaped();
529
		$displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
530
			->rawParams(
531
				$language->listToText( $autoList ),
532
				$language->listToText( $autoMembersList )
533
			)->escaped();
534
535
		$grouplist = '';
536
		$count = count( $list );
537 View Code Duplication
		if ( $count > 0 ) {
538
			$grouplist = $this->msg( 'userrights-groupsmember' )
539
				->numParams( $count )
540
				->params( $user->getName() )
541
				->parse();
542
			$grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
543
		}
544
545
		$count = count( $autoList );
546 View Code Duplication
		if ( $count > 0 ) {
547
			$autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
548
				->numParams( $count )
549
				->params( $user->getName() )
550
				->parse();
551
			$grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
552
		}
553
554
		$userToolLinks = Linker::userToolLinks(
555
			$user->getId(),
556
			$user->getName(),
557
			false, /* default for redContribsWhenNoEdits */
558
			Linker::TOOL_LINKS_EMAIL /* Add "send e-mail" link */
559
		);
560
561
		$this->getOutput()->addHTML(
562
			Xml::openElement(
563
				'form',
564
				[
565
					'method' => 'post',
566
					'action' => $this->getPageTitle()->getLocalURL(),
567
					'name' => 'editGroup',
568
					'id' => 'mw-userrights-form2'
569
				]
570
			) .
571
			Html::hidden( 'user', $this->mTarget ) .
572
			Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
573
			Html::hidden(
574
				'conflictcheck-originalgroups',
575
				implode( ',', $user->getGroups() )
576
			) . // Conflict detection
577
			Xml::openElement( 'fieldset' ) .
578
			Xml::element(
579
				'legend',
580
				[],
581
				$this->msg( 'userrights-editusergroup', $user->getName() )->text()
582
			) .
583
			$this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )
584
				->rawParams( $userToolLinks )->parse() .
585
			$this->msg( 'userrights-groups-help', $user->getName() )->parse() .
586
			$grouplist .
587
			$this->groupCheckboxes( $groups, $user ) .
0 ignored issues
show
Bug introduced by
It seems like $user defined by parameter $user on line 506 can also be of type object<UserRightsProxy>; however, UserrightsPage::groupCheckboxes() does only seem to accept object<User>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
588
			Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
589
				"<tr>
590
					<td class='mw-label'>" .
591
						Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
592
					"</td>
593
					<td class='mw-input'>" .
594
						Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ),
595
							[ 'id' => 'wpReason', 'maxlength' => 255 ] ) .
596
					"</td>
597
				</tr>
598
				<tr>
599
					<td></td>
600
					<td class='mw-submit'>" .
601
						Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
602
							[ 'name' => 'saveusergroups' ] +
603
								Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
604
						) .
605
					"</td>
606
				</tr>" .
607
			Xml::closeElement( 'table' ) . "\n" .
608
			Xml::closeElement( 'fieldset' ) .
609
			Xml::closeElement( 'form' ) . "\n"
610
		);
611
	}
612
613
	/**
614
	 * Format a link to a group description page
615
	 *
616
	 * @param string $group
617
	 * @return string
618
	 */
619
	private static function buildGroupLink( $group ) {
620
		return User::makeGroupLinkHTML( $group, User::getGroupName( $group ) );
621
	}
622
623
	/**
624
	 * Format a link to a group member description page
625
	 *
626
	 * @param string $group
627
	 * @return string
628
	 */
629
	private static function buildGroupMemberLink( $group ) {
630
		return User::makeGroupLinkHTML( $group, User::getGroupMember( $group ) );
631
	}
632
633
	/**
634
	 * Returns an array of all groups that may be edited
635
	 * @return array Array of groups that may be edited.
636
	 */
637
	protected static function getAllGroups() {
638
		return User::getAllGroups();
639
	}
640
641
	/**
642
	 * Adds a table with checkboxes where you can select what groups to add/remove
643
	 *
644
	 * @todo Just pass the username string?
645
	 * @param array $usergroups Groups the user belongs to
646
	 * @param User $user
647
	 * @return string XHTML table element with checkboxes
648
	 */
649
	private function groupCheckboxes( $usergroups, $user ) {
650
		$allgroups = $this->getAllGroups();
651
		$ret = '';
652
653
		// Put all column info into an associative array so that extensions can
654
		// more easily manage it.
655
		$columns = [ 'unchangeable' => [], 'changeable' => [] ];
656
657
		foreach ( $allgroups as $group ) {
658
			$set = in_array( $group, $usergroups );
659
			// Should the checkbox be disabled?
660
			$disabled = !(
661
				( $set && $this->canRemove( $group ) ) ||
662
				( !$set && $this->canAdd( $group ) ) );
663
			// Do we need to point out that this action is irreversible?
664
			$irreversible = !$disabled && (
665
				( $set && !$this->canAdd( $group ) ) ||
666
				( !$set && !$this->canRemove( $group ) ) );
667
668
			$checkbox = [
669
				'set' => $set,
670
				'disabled' => $disabled,
671
				'irreversible' => $irreversible
672
			];
673
674
			if ( $disabled ) {
675
				$columns['unchangeable'][$group] = $checkbox;
676
			} else {
677
				$columns['changeable'][$group] = $checkbox;
678
			}
679
		}
680
681
		// Build the HTML table
682
		$ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
683
			"<tr>\n";
684
		foreach ( $columns as $name => $column ) {
685
			if ( $column === [] ) {
686
				continue;
687
			}
688
			// Messages: userrights-changeable-col, userrights-unchangeable-col
689
			$ret .= Xml::element(
690
				'th',
691
				null,
692
				$this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
693
			);
694
		}
695
696
		$ret .= "</tr>\n<tr>\n";
697
		foreach ( $columns as $column ) {
698
			if ( $column === [] ) {
699
				continue;
700
			}
701
			$ret .= "\t<td style='vertical-align:top;'>\n";
702
			foreach ( $column as $group => $checkbox ) {
703
				$attr = $checkbox['disabled'] ? [ 'disabled' => 'disabled' ] : [];
704
705
				$member = User::getGroupMember( $group, $user->getName() );
706
				if ( $checkbox['irreversible'] ) {
707
					$text = $this->msg( 'userrights-irreversible-marker', $member )->text();
708
				} else {
709
					$text = $member;
710
				}
711
				$checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
712
					"wpGroup-" . $group, $checkbox['set'], $attr );
713
				$ret .= "\t\t" . ( $checkbox['disabled']
714
					? Xml::tags( 'span', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
715
					: $checkboxHtml
716
				) . "<br />\n";
717
			}
718
			$ret .= "\t</td>\n";
719
		}
720
		$ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
721
722
		return $ret;
723
	}
724
725
	/**
726
	 * @param string $group The name of the group to check
727
	 * @return bool Can we remove the group?
728
	 */
729 View Code Duplication
	private function canRemove( $group ) {
730
		// $this->changeableGroups()['remove'] doesn't work, of course. Thanks, PHP.
731
		$groups = $this->changeableGroups();
732
733
		return in_array(
734
			$group,
735
			$groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
736
		);
737
	}
738
739
	/**
740
	 * @param string $group The name of the group to check
741
	 * @return bool Can we add the group?
742
	 */
743 View Code Duplication
	private function canAdd( $group ) {
744
		$groups = $this->changeableGroups();
745
746
		return in_array(
747
			$group,
748
			$groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
749
		);
750
	}
751
752
	/**
753
	 * Returns $this->getUser()->changeableGroups()
754
	 *
755
	 * @return array Array(
756
	 *   'add' => array( addablegroups ),
757
	 *   'remove' => array( removablegroups ),
758
	 *   'add-self' => array( addablegroups to self ),
759
	 *   'remove-self' => array( removable groups from self )
760
	 *  )
761
	 */
762
	function changeableGroups() {
763
		return $this->getUser()->changeableGroups();
764
	}
765
766
	/**
767
	 * Show a rights log fragment for the specified user
768
	 *
769
	 * @param User $user User to show log for
770
	 * @param OutputPage $output OutputPage to use
771
	 */
772
	protected function showLogFragment( $user, $output ) {
773
		$rightsLogPage = new LogPage( 'rights' );
774
		$output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
775
		LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
776
	}
777
778
	/**
779
	 * Return an array of subpages beginning with $search that this special page will accept.
780
	 *
781
	 * @param string $search Prefix to search for
782
	 * @param int $limit Maximum number of results to return (usually 10)
783
	 * @param int $offset Number of results to skip (usually 0)
784
	 * @return string[] Matching subpages
785
	 */
786 View Code Duplication
	public function prefixSearchSubpages( $search, $limit, $offset ) {
787
		$user = User::newFromName( $search );
788
		if ( !$user ) {
789
			// No prefix suggestion for invalid user
790
			return [];
791
		}
792
		// Autocomplete subpage as user list - public to allow caching
793
		return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
794
	}
795
796
	protected function getGroupName() {
797
		return 'users';
798
	}
799
}
800