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/SpecialUserrights.php (5 issues)

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: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
	/**
31
	 * The target of the local right-adjuster's interest.  Can be gotten from
32
	 * either a GET parameter or a subpage-style parameter, so have a member
33
	 * variable for it.
34
	 * @var null|string $mTarget
35
	 */
36
	protected $mTarget;
37
	/*
38
	 * @var null|User $mFetchedUser The user object of the target username or null.
39
	 */
40
	protected $mFetchedUser = null;
41
	protected $isself = false;
42
43
	public function __construct() {
44
		parent::__construct( 'Userrights' );
45
	}
46
47
	public function doesWrites() {
48
		return true;
49
	}
50
51
	public function isRestricted() {
52
		return true;
53
	}
54
55
	public function userCanExecute( User $user ) {
56
		return $this->userCanChangeRights( $user, false );
57
	}
58
59
	/**
60
	 * @param User $user
61
	 * @param bool $checkIfSelf
62
	 * @return bool
63
	 */
64
	public function userCanChangeRights( $user, $checkIfSelf = true ) {
65
		$available = $this->changeableGroups();
66
		if ( $user->getId() == 0 ) {
67
			return false;
68
		}
69
70
		return !empty( $available['add'] )
71
			|| !empty( $available['remove'] )
72
			|| ( ( $this->isself || !$checkIfSelf ) &&
73
				( !empty( $available['add-self'] )
74
					|| !empty( $available['remove-self'] ) ) );
75
	}
76
77
	/**
78
	 * Manage forms to be shown according to posted data.
79
	 * Depending on the submit button used, call a form or a save function.
80
	 *
81
	 * @param string|null $par String if any subpage provided, else null
82
	 * @throws UserBlockedError|PermissionsError
83
	 */
84
	public function execute( $par ) {
85
		// If the visitor doesn't have permissions to assign or remove
86
		// any groups, it's a bit silly to give them the user search prompt.
87
88
		$user = $this->getUser();
89
		$request = $this->getRequest();
90
		$out = $this->getOutput();
91
92
		/*
93
		 * If the user is blocked and they only have "partial" access
94
		 * (e.g. they don't have the userrights permission), then don't
95
		 * allow them to use Special:UserRights.
96
		 */
97
		if ( $user->isBlocked() && !$user->isAllowed( 'userrights' ) ) {
98
			throw new UserBlockedError( $user->getBlock() );
0 ignored issues
show
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...
99
		}
100
101 View Code Duplication
		if ( $par !== null ) {
102
			$this->mTarget = $par;
103
		} else {
104
			$this->mTarget = $request->getVal( 'user' );
105
		}
106
107
		if ( is_string( $this->mTarget ) ) {
108
			$this->mTarget = trim( $this->mTarget );
109
		}
110
111
		$available = $this->changeableGroups();
112
113
		if ( $this->mTarget === null ) {
114
			/*
115
			 * If the user specified no target, and they can only
116
			 * edit their own groups, automatically set them as the
117
			 * target.
118
			 */
119
			if ( !count( $available['add'] ) && !count( $available['remove'] ) ) {
120
				$this->mTarget = $user->getName();
121
			}
122
		}
123
124
		if ( $this->mTarget !== null && User::getCanonicalName( $this->mTarget ) === $user->getName() ) {
125
			$this->isself = true;
126
		}
127
128
		$fetchedStatus = $this->fetchUser( $this->mTarget );
129
		if ( $fetchedStatus->isOK() ) {
130
			$this->mFetchedUser = $fetchedStatus->value;
131
			if ( $this->mFetchedUser instanceof User ) {
132
				// Set the 'relevant user' in the skin, so it displays links like Contributions,
133
				// User logs, UserRights, etc.
134
				$this->getSkin()->setRelevantUser( $this->mFetchedUser );
135
			}
136
		}
137
138
		if ( !$this->userCanChangeRights( $user, true ) ) {
139
			if ( $this->isself && $request->getCheck( 'success' ) ) {
140
				// bug 48609: if the user just removed its own rights, this would
141
				// leads it in a "permissions error" page. In that case, show a
142
				// message that it can't anymore use this page instead of an error
143
				$this->setHeaders();
144
				$out->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>", 'userrights-removed-self' );
145
				$out->returnToMain();
146
147
				return;
148
			}
149
150
			// @todo FIXME: There may be intermediate groups we can mention.
151
			$msg = $user->isAnon() ? 'userrights-nologin' : 'userrights-notallowed';
152
			throw new PermissionsError( null, [ [ $msg ] ] );
153
		}
154
155
		// show a successbox, if the user rights was saved successfully
156
		if ( $request->getCheck( 'success' ) && $this->mFetchedUser !== null ) {
157
			$out->addModules( [ 'mediawiki.special.userrights' ] );
158
			$out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
159
			$out->addHTML(
160
				Html::rawElement(
161
					'div',
162
					[
163
						'class' => 'mw-notify-success successbox',
164
						'id' => 'mw-preferences-success',
165
						'data-mw-autohide' => 'false',
166
					],
167
					Html::element(
168
						'p',
169
						[],
170
						$this->msg( 'savedrights', $this->mFetchedUser->getName() )->text()
171
					)
172
				)
173
			);
174
		}
175
176
		$this->checkReadOnly();
177
178
		$this->setHeaders();
179
		$this->outputHeader();
180
181
		$out->addModuleStyles( 'mediawiki.special' );
182
		$this->addHelpLink( 'Help:Assigning permissions' );
183
184
		// show the general form
185
		if ( count( $available['add'] ) || count( $available['remove'] ) ) {
186
			$this->switchForm();
187
		}
188
189
		if (
190
			$request->wasPosted() &&
191
			$request->getCheck( 'saveusergroups' ) &&
192
			$this->mTarget !== null &&
193
			$user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
194
		) {
195
			// save settings
196
			if ( !$fetchedStatus->isOK() ) {
197
				$this->getOutput()->addWikiText( $fetchedStatus->getWikiText() );
198
199
				return;
200
			}
201
202
			$targetUser = $this->mFetchedUser;
203
			if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (bug 61252)
204
				$targetUser->clearInstanceCache(); // bug 38989
205
			}
206
207
			if ( $request->getVal( 'conflictcheck-originalgroups' )
208
				!== implode( ',', $targetUser->getGroups() )
209
			) {
210
				$out->addWikiMsg( 'userrights-conflict' );
211
			} else {
212
				$this->saveUserGroups(
213
					$this->mTarget,
214
					$request->getVal( 'user-reason' ),
215
					$targetUser
216
				);
217
218
				$out->redirect( $this->getSuccessURL() );
219
220
				return;
221
			}
222
		}
223
224
		// show some more forms
225
		if ( $this->mTarget !== null ) {
226
			$this->editUserGroupsForm( $this->mTarget );
227
		}
228
	}
229
230
	function getSuccessURL() {
231
		return $this->getPageTitle( $this->mTarget )->getFullURL( [ 'success' => 1 ] );
232
	}
233
234
	/**
235
	 * Save user groups changes in the database.
236
	 * Data comes from the editUserGroupsForm() form function
237
	 *
238
	 * @param string $username Username to apply changes to.
239
	 * @param string $reason Reason for group change
240
	 * @param User|UserRightsProxy $user Target user object.
241
	 * @return null
242
	 */
243
	function saveUserGroups( $username, $reason, $user ) {
244
		$allgroups = $this->getAllGroups();
245
		$addgroup = [];
246
		$removegroup = [];
247
248
		// This could possibly create a highly unlikely race condition if permissions are changed between
249
		//  when the form is loaded and when the form is saved. Ignoring it for the moment.
250
		foreach ( $allgroups as $group ) {
251
			// We'll tell it to remove all unchecked groups, and add all checked groups.
252
			// Later on, this gets filtered for what can actually be removed
253
			if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
254
				$addgroup[] = $group;
255
			} else {
256
				$removegroup[] = $group;
257
			}
258
		}
259
260
		$this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason );
261
	}
262
263
	/**
264
	 * Save user groups changes in the database.
265
	 *
266
	 * @param User|UserRightsProxy $user
267
	 * @param array $add Array of groups to add
268
	 * @param array $remove Array of groups to remove
269
	 * @param string $reason Reason for group change
270
	 * @return array Tuple of added, then removed groups
271
	 */
272
	function doSaveUserGroups( $user, $add, $remove, $reason = '' ) {
273
		// Validate input set...
274
		$isself = $user->getName() == $this->getUser()->getName();
275
		$groups = $user->getGroups();
276
		$changeable = $this->changeableGroups();
277
		$addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
278
		$removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
279
280
		$remove = array_unique(
281
			array_intersect( (array)$remove, $removable, $groups ) );
282
		$add = array_unique( array_diff(
283
			array_intersect( (array)$add, $addable ),
284
			$groups )
285
		);
286
287
		$oldGroups = $user->getGroups();
288
		$newGroups = $oldGroups;
289
290
		// Remove then add groups
291 View Code Duplication
		if ( $remove ) {
292
			foreach ( $remove as $index => $group ) {
293
				if ( !$user->removeGroup( $group ) ) {
294
					unset( $remove[$index] );
295
				}
296
			}
297
			$newGroups = array_diff( $newGroups, $remove );
298
		}
299 View Code Duplication
		if ( $add ) {
300
			foreach ( $add as $index => $group ) {
301
				if ( !$user->addGroup( $group ) ) {
302
					unset( $add[$index] );
303
				}
304
			}
305
			$newGroups = array_merge( $newGroups, $add );
306
		}
307
		$newGroups = array_unique( $newGroups );
308
309
		// Ensure that caches are cleared
310
		$user->invalidateCache();
311
312
		// update groups in external authentication database
313
		Hooks::run( 'UserGroupsChanged', [ $user, $add, $remove, $this->getUser(), $reason ] );
314
		MediaWiki\Auth\AuthManager::callLegacyAuthPlugin(
315
			'updateExternalDBGroups', [ $user, $add, $remove ]
316
		);
317
318
		wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" );
319
		wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" );
320
		// Deprecated in favor of UserGroupsChanged hook
321
		Hooks::run( 'UserRights', [ &$user, $add, $remove ], '1.26' );
322
323
		if ( $newGroups != $oldGroups ) {
324
			$this->addLogEntry( $user, $oldGroups, $newGroups, $reason );
0 ignored issues
show
It seems like $user defined by parameter $user on line 272 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...
325
		}
326
327
		return [ $add, $remove ];
328
	}
329
330
	/**
331
	 * Add a rights log entry for an action.
332
	 * @param User $user
333
	 * @param array $oldGroups
334
	 * @param array $newGroups
335
	 * @param array $reason
336
	 */
337
	function addLogEntry( $user, $oldGroups, $newGroups, $reason ) {
338
		$logEntry = new ManualLogEntry( 'rights', 'rights' );
339
		$logEntry->setPerformer( $this->getUser() );
340
		$logEntry->setTarget( $user->getUserPage() );
341
		$logEntry->setComment( $reason );
342
		$logEntry->setParameters( [
343
			'4::oldgroups' => $oldGroups,
344
			'5::newgroups' => $newGroups,
345
		] );
346
		$logid = $logEntry->insert();
347
		$logEntry->publish( $logid );
348
	}
349
350
	/**
351
	 * Edit user groups membership
352
	 * @param string $username Name of the user.
353
	 */
354
	function editUserGroupsForm( $username ) {
355
		$status = $this->fetchUser( $username );
356
		if ( !$status->isOK() ) {
357
			$this->getOutput()->addWikiText( $status->getWikiText() );
358
359
			return;
360
		} else {
361
			$user = $status->value;
362
		}
363
364
		$groups = $user->getGroups();
365
366
		$this->showEditUserGroupsForm( $user, $groups );
367
368
		// This isn't really ideal logging behavior, but let's not hide the
369
		// interwiki logs if we're using them as is.
370
		$this->showLogFragment( $user, $this->getOutput() );
371
	}
372
373
	/**
374
	 * Normalize the input username, which may be local or remote, and
375
	 * return a user (or proxy) object for manipulating it.
376
	 *
377
	 * Side effects: error output for invalid access
378
	 * @param string $username
379
	 * @return Status
380
	 */
381
	public function fetchUser( $username ) {
382
		$parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username );
383
		if ( count( $parts ) < 2 ) {
384
			$name = trim( $username );
385
			$database = '';
386
		} else {
387
			list( $name, $database ) = array_map( 'trim', $parts );
388
389
			if ( $database == wfWikiID() ) {
390
				$database = '';
391
			} else {
392
				if ( !$this->getUser()->isAllowed( 'userrights-interwiki' ) ) {
393
					return Status::newFatal( 'userrights-no-interwiki' );
394
				}
395
				if ( !UserRightsProxy::validDatabase( $database ) ) {
396
					return Status::newFatal( 'userrights-nodatabase', $database );
397
				}
398
			}
399
		}
400
401
		if ( $name === '' ) {
402
			return Status::newFatal( 'nouserspecified' );
403
		}
404
405
		if ( $name[0] == '#' ) {
406
			// Numeric ID can be specified...
407
			// We'll do a lookup for the name internally.
408
			$id = intval( substr( $name, 1 ) );
409
410
			if ( $database == '' ) {
411
				$name = User::whoIs( $id );
412
			} else {
413
				$name = UserRightsProxy::whoIs( $database, $id );
414
			}
415
416
			if ( !$name ) {
417
				return Status::newFatal( 'noname' );
418
			}
419
		} else {
420
			$name = User::getCanonicalName( $name );
421
			if ( $name === false ) {
422
				// invalid name
423
				return Status::newFatal( 'nosuchusershort', $username );
424
			}
425
		}
426
427
		if ( $database == '' ) {
428
			$user = User::newFromName( $name );
0 ignored issues
show
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...
429
		} else {
430
			$user = UserRightsProxy::newFromName( $database, $name );
0 ignored issues
show
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...
431
		}
432
433
		if ( !$user || $user->isAnon() ) {
434
			return Status::newFatal( 'nosuchusershort', $username );
435
		}
436
437
		return Status::newGood( $user );
438
	}
439
440
	/**
441
	 * @since 1.15
442
	 *
443
	 * @param array $ids
444
	 *
445
	 * @return string
446
	 */
447
	public function makeGroupNameList( $ids ) {
448
		if ( empty( $ids ) ) {
449
			return $this->msg( 'rightsnone' )->inContentLanguage()->text();
450
		} else {
451
			return implode( ', ', $ids );
452
		}
453
	}
454
455
	/**
456
	 * Output a form to allow searching for a user
457
	 */
458
	function switchForm() {
459
		$this->getOutput()->addModules( 'mediawiki.userSuggest' );
460
461
		$this->getOutput()->addHTML(
462
			Html::openElement(
463
				'form',
464
				[
465
					'method' => 'get',
466
					'action' => wfScript(),
467
					'name' => 'uluser',
468
					'id' => 'mw-userrights-form1'
469
				]
470
			) .
471
			Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
472
			Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
473
			Xml::inputLabel(
474
				$this->msg( 'userrights-user-editname' )->text(),
475
				'user',
476
				'username',
477
				30,
478
				str_replace( '_', ' ', $this->mTarget ),
479
				[
480
					'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
481
				] + (
482
					// Set autofocus on blank input and error input
483
					$this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
484
				)
485
			) . ' ' .
486
			Xml::submitButton(
487
				$this->msg(
488
					'editusergroup',
489
					$this->mFetchedUser === null ? '[]' : $this->mFetchedUser->getName()
490
				)->text()
491
			) .
492
			Html::closeElement( 'fieldset' ) .
493
			Html::closeElement( 'form' ) . "\n"
494
		);
495
	}
496
497
	/**
498
	 * Go through used and available groups and return the ones that this
499
	 * form will be able to manipulate based on the current user's system
500
	 * permissions.
501
	 *
502
	 * @param array $groups List of groups the given user is in
503
	 * @return array Tuple of addable, then removable groups
504
	 */
505
	protected function splitGroups( $groups ) {
506
		list( $addable, $removable, $addself, $removeself ) = array_values( $this->changeableGroups() );
507
508
		$removable = array_intersect(
509
			array_merge( $this->isself ? $removeself : [], $removable ),
510
			$groups
511
		); // Can't remove groups the user doesn't have
512
		$addable = array_diff(
513
			array_merge( $this->isself ? $addself : [], $addable ),
514
			$groups
515
		); // Can't add groups the user does have
516
517
		return [ $addable, $removable ];
518
	}
519
520
	/**
521
	 * Show the form to edit group memberships.
522
	 *
523
	 * @param User|UserRightsProxy $user User or UserRightsProxy you're editing
524
	 * @param array $groups Array of groups the user is in
525
	 */
526
	protected function showEditUserGroupsForm( $user, $groups ) {
527
		$list = [];
528
		$membersList = [];
529
		foreach ( $groups as $group ) {
530
			$list[] = self::buildGroupLink( $group );
531
			$membersList[] = self::buildGroupMemberLink( $group );
532
		}
533
534
		$autoList = [];
535
		$autoMembersList = [];
536
		if ( $user instanceof User ) {
537
			foreach ( Autopromote::getAutopromoteGroups( $user ) as $group ) {
538
				$autoList[] = self::buildGroupLink( $group );
539
				$autoMembersList[] = self::buildGroupMemberLink( $group );
540
			}
541
		}
542
543
		$language = $this->getLanguage();
544
		$displayedList = $this->msg( 'userrights-groupsmember-type' )
545
			->rawParams(
546
				$language->listToText( $list ),
547
				$language->listToText( $membersList )
548
			)->escaped();
549
		$displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
550
			->rawParams(
551
				$language->listToText( $autoList ),
552
				$language->listToText( $autoMembersList )
553
			)->escaped();
554
555
		$grouplist = '';
556
		$count = count( $list );
557 View Code Duplication
		if ( $count > 0 ) {
558
			$grouplist = $this->msg( 'userrights-groupsmember' )
559
				->numParams( $count )
560
				->params( $user->getName() )
561
				->parse();
562
			$grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
563
		}
564
565
		$count = count( $autoList );
566 View Code Duplication
		if ( $count > 0 ) {
567
			$autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
568
				->numParams( $count )
569
				->params( $user->getName() )
570
				->parse();
571
			$grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
572
		}
573
574
		$userToolLinks = Linker::userToolLinks(
575
			$user->getId(),
576
			$user->getName(),
577
			false, /* default for redContribsWhenNoEdits */
578
			Linker::TOOL_LINKS_EMAIL /* Add "send e-mail" link */
579
		);
580
581
		$this->getOutput()->addHTML(
582
			Xml::openElement(
583
				'form',
584
				[
585
					'method' => 'post',
586
					'action' => $this->getPageTitle()->getLocalURL(),
587
					'name' => 'editGroup',
588
					'id' => 'mw-userrights-form2'
589
				]
590
			) .
591
			Html::hidden( 'user', $this->mTarget ) .
592
			Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
593
			Html::hidden(
594
				'conflictcheck-originalgroups',
595
				implode( ',', $user->getGroups() )
596
			) . // Conflict detection
597
			Xml::openElement( 'fieldset' ) .
598
			Xml::element(
599
				'legend',
600
				[],
601
				$this->msg( 'userrights-editusergroup', $user->getName() )->text()
602
			) .
603
			$this->msg( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )
604
				->rawParams( $userToolLinks )->parse() .
605
			$this->msg( 'userrights-groups-help', $user->getName() )->parse() .
606
			$grouplist .
607
			$this->groupCheckboxes( $groups, $user ) .
0 ignored issues
show
It seems like $user defined by parameter $user on line 526 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...
608
			Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
609
				"<tr>
610
					<td class='mw-label'>" .
611
						Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
612
					"</td>
613
					<td class='mw-input'>" .
614
						Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ),
615
							[ 'id' => 'wpReason', 'maxlength' => 255 ] ) .
616
					"</td>
617
				</tr>
618
				<tr>
619
					<td></td>
620
					<td class='mw-submit'>" .
621
						Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
622
							[ 'name' => 'saveusergroups' ] +
623
								Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
624
						) .
625
					"</td>
626
				</tr>" .
627
			Xml::closeElement( 'table' ) . "\n" .
628
			Xml::closeElement( 'fieldset' ) .
629
			Xml::closeElement( 'form' ) . "\n"
630
		);
631
	}
632
633
	/**
634
	 * Format a link to a group description page
635
	 *
636
	 * @param string $group
637
	 * @return string
638
	 */
639
	private static function buildGroupLink( $group ) {
640
		return User::makeGroupLinkHTML( $group, User::getGroupName( $group ) );
641
	}
642
643
	/**
644
	 * Format a link to a group member description page
645
	 *
646
	 * @param string $group
647
	 * @return string
648
	 */
649
	private static function buildGroupMemberLink( $group ) {
650
		return User::makeGroupLinkHTML( $group, User::getGroupMember( $group ) );
651
	}
652
653
	/**
654
	 * Returns an array of all groups that may be edited
655
	 * @return array Array of groups that may be edited.
656
	 */
657
	protected static function getAllGroups() {
658
		return User::getAllGroups();
659
	}
660
661
	/**
662
	 * Adds a table with checkboxes where you can select what groups to add/remove
663
	 *
664
	 * @todo Just pass the username string?
665
	 * @param array $usergroups Groups the user belongs to
666
	 * @param User $user
667
	 * @return string XHTML table element with checkboxes
668
	 */
669
	private function groupCheckboxes( $usergroups, $user ) {
670
		$allgroups = $this->getAllGroups();
671
		$ret = '';
672
673
		// Put all column info into an associative array so that extensions can
674
		// more easily manage it.
675
		$columns = [ 'unchangeable' => [], 'changeable' => [] ];
676
677
		foreach ( $allgroups as $group ) {
678
			$set = in_array( $group, $usergroups );
679
			// Should the checkbox be disabled?
680
			$disabled = !(
681
				( $set && $this->canRemove( $group ) ) ||
682
				( !$set && $this->canAdd( $group ) ) );
683
			// Do we need to point out that this action is irreversible?
684
			$irreversible = !$disabled && (
685
				( $set && !$this->canAdd( $group ) ) ||
686
				( !$set && !$this->canRemove( $group ) ) );
687
688
			$checkbox = [
689
				'set' => $set,
690
				'disabled' => $disabled,
691
				'irreversible' => $irreversible
692
			];
693
694
			if ( $disabled ) {
695
				$columns['unchangeable'][$group] = $checkbox;
696
			} else {
697
				$columns['changeable'][$group] = $checkbox;
698
			}
699
		}
700
701
		// Build the HTML table
702
		$ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
703
			"<tr>\n";
704
		foreach ( $columns as $name => $column ) {
705
			if ( $column === [] ) {
706
				continue;
707
			}
708
			// Messages: userrights-changeable-col, userrights-unchangeable-col
709
			$ret .= Xml::element(
710
				'th',
711
				null,
712
				$this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
713
			);
714
		}
715
716
		$ret .= "</tr>\n<tr>\n";
717
		foreach ( $columns as $column ) {
718
			if ( $column === [] ) {
719
				continue;
720
			}
721
			$ret .= "\t<td style='vertical-align:top;'>\n";
722
			foreach ( $column as $group => $checkbox ) {
723
				$attr = $checkbox['disabled'] ? [ 'disabled' => 'disabled' ] : [];
724
725
				$member = User::getGroupMember( $group, $user->getName() );
726
				if ( $checkbox['irreversible'] ) {
727
					$text = $this->msg( 'userrights-irreversible-marker', $member )->text();
728
				} else {
729
					$text = $member;
730
				}
731
				$checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
732
					"wpGroup-" . $group, $checkbox['set'], $attr );
733
				$ret .= "\t\t" . ( $checkbox['disabled']
734
					? Xml::tags( 'span', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
735
					: $checkboxHtml
736
				) . "<br />\n";
737
			}
738
			$ret .= "\t</td>\n";
739
		}
740
		$ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
741
742
		return $ret;
743
	}
744
745
	/**
746
	 * @param string $group The name of the group to check
747
	 * @return bool Can we remove the group?
748
	 */
749 View Code Duplication
	private function canRemove( $group ) {
750
		// $this->changeableGroups()['remove'] doesn't work, of course. Thanks, PHP.
751
		$groups = $this->changeableGroups();
752
753
		return in_array(
754
			$group,
755
			$groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
756
		);
757
	}
758
759
	/**
760
	 * @param string $group The name of the group to check
761
	 * @return bool Can we add the group?
762
	 */
763 View Code Duplication
	private function canAdd( $group ) {
764
		$groups = $this->changeableGroups();
765
766
		return in_array(
767
			$group,
768
			$groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
769
		);
770
	}
771
772
	/**
773
	 * Returns $this->getUser()->changeableGroups()
774
	 *
775
	 * @return array Array(
776
	 *   'add' => array( addablegroups ),
777
	 *   'remove' => array( removablegroups ),
778
	 *   'add-self' => array( addablegroups to self ),
779
	 *   'remove-self' => array( removable groups from self )
780
	 *  )
781
	 */
782
	function changeableGroups() {
783
		return $this->getUser()->changeableGroups();
784
	}
785
786
	/**
787
	 * Show a rights log fragment for the specified user
788
	 *
789
	 * @param User $user User to show log for
790
	 * @param OutputPage $output OutputPage to use
791
	 */
792
	protected function showLogFragment( $user, $output ) {
793
		$rightsLogPage = new LogPage( 'rights' );
794
		$output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
795
		LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
796
	}
797
798
	/**
799
	 * Return an array of subpages beginning with $search that this special page will accept.
800
	 *
801
	 * @param string $search Prefix to search for
802
	 * @param int $limit Maximum number of results to return (usually 10)
803
	 * @param int $offset Number of results to skip (usually 0)
804
	 * @return string[] Matching subpages
805
	 */
806 View Code Duplication
	public function prefixSearchSubpages( $search, $limit, $offset ) {
807
		$user = User::newFromName( $search );
808
		if ( !$user ) {
809
			// No prefix suggestion for invalid user
810
			return [];
811
		}
812
		// Autocomplete subpage as user list - public to allow caching
813
		return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
814
	}
815
816
	protected function getGroupName() {
817
		return 'users';
818
	}
819
}
820