Ensure block hooks keep user state consistent with realistic blocks
[lhc/web/wiklou.git] / includes / Permissions / PermissionManager.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20 namespace MediaWiki\Permissions;
21
22 use Action;
23 use Exception;
24 use Hooks;
25 use MediaWiki\Linker\LinkTarget;
26 use MediaWiki\Revision\RevisionLookup;
27 use MediaWiki\Revision\RevisionRecord;
28 use MediaWiki\Session\SessionManager;
29 use MediaWiki\Special\SpecialPageFactory;
30 use MediaWiki\User\UserIdentity;
31 use MessageSpecifier;
32 use NamespaceInfo;
33 use RequestContext;
34 use SpecialPage;
35 use Title;
36 use User;
37 use Wikimedia\ScopedCallback;
38 use WikiPage;
39
40 /**
41 * A service class for checking permissions
42 * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
43 *
44 * @since 1.33
45 */
46 class PermissionManager {
47
48 /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
49 const RIGOR_QUICK = 'quick';
50
51 /** @var string Does cheap and expensive checks possibly from a replica DB */
52 const RIGOR_FULL = 'full';
53
54 /** @var string Does cheap and expensive checks, using the master as needed */
55 const RIGOR_SECURE = 'secure';
56
57 /** @var SpecialPageFactory */
58 private $specialPageFactory;
59
60 /** @var RevisionLookup */
61 private $revisionLookup;
62
63 /** @var string[] List of pages names anonymous user may see */
64 private $whitelistRead;
65
66 /** @var string[] Whitelists publicly readable titles with regular expressions */
67 private $whitelistReadRegexp;
68
69 /** @var bool Require users to confirm email address before they can edit */
70 private $emailConfirmToEdit;
71
72 /** @var bool If set to true, blocked users will no longer be allowed to log in */
73 private $blockDisablesLogin;
74
75 /** @var NamespaceInfo */
76 private $nsInfo;
77
78 /** @var string[][] Access rights for groups and users in these groups */
79 private $groupPermissions;
80
81 /** @var string[][] Permission keys revoked from users in each group */
82 private $revokePermissions;
83
84 /** @var string[] A list of available rights, in addition to the ones defined by the core */
85 private $availableRights;
86
87 /** @var string[] Cached results of getAllRights() */
88 private $allRights = false;
89
90 /** @var string[][] Cached user rights */
91 private $usersRights = null;
92
93 /**
94 * Temporary user rights, valid for the current request only.
95 * @var string[][][] userid => override group => rights
96 */
97 private $temporaryUserRights = [];
98
99 /** @var string[] Cached rights for isEveryoneAllowed */
100 private $cachedRights = [];
101
102 /**
103 * Array of Strings Core rights.
104 * Each of these should have a corresponding message of the form
105 * "right-$right".
106 * @showinitializer
107 */
108 private $coreRights = [
109 'apihighlimits',
110 'applychangetags',
111 'autoconfirmed',
112 'autocreateaccount',
113 'autopatrol',
114 'bigdelete',
115 'block',
116 'blockemail',
117 'bot',
118 'browsearchive',
119 'changetags',
120 'createaccount',
121 'createpage',
122 'createtalk',
123 'delete',
124 'deletechangetags',
125 'deletedhistory',
126 'deletedtext',
127 'deletelogentry',
128 'deleterevision',
129 'edit',
130 'editcontentmodel',
131 'editinterface',
132 'editprotected',
133 'editmyoptions',
134 'editmyprivateinfo',
135 'editmyusercss',
136 'editmyuserjson',
137 'editmyuserjs',
138 'editmyuserjsredirect',
139 'editmywatchlist',
140 'editsemiprotected',
141 'editsitecss',
142 'editsitejson',
143 'editsitejs',
144 'editusercss',
145 'edituserjson',
146 'edituserjs',
147 'hideuser',
148 'import',
149 'importupload',
150 'ipblock-exempt',
151 'managechangetags',
152 'markbotedits',
153 'mergehistory',
154 'minoredit',
155 'move',
156 'movefile',
157 'move-categorypages',
158 'move-rootuserpages',
159 'move-subpages',
160 'nominornewtalk',
161 'noratelimit',
162 'override-export-depth',
163 'pagelang',
164 'patrol',
165 'patrolmarks',
166 'protect',
167 'purge',
168 'read',
169 'reupload',
170 'reupload-own',
171 'reupload-shared',
172 'rollback',
173 'sendemail',
174 'siteadmin',
175 'suppressionlog',
176 'suppressredirect',
177 'suppressrevision',
178 'unblockself',
179 'undelete',
180 'unwatchedpages',
181 'upload',
182 'upload_by_url',
183 'userrights',
184 'userrights-interwiki',
185 'viewmyprivateinfo',
186 'viewmywatchlist',
187 'viewsuppressed',
188 'writeapi',
189 ];
190
191 /**
192 * @param SpecialPageFactory $specialPageFactory
193 * @param RevisionLookup $revisionLookup
194 * @param string[] $whitelistRead
195 * @param string[] $whitelistReadRegexp
196 * @param bool $emailConfirmToEdit
197 * @param bool $blockDisablesLogin
198 * @param string[][] $groupPermissions
199 * @param string[][] $revokePermissions
200 * @param string[] $availableRights
201 * @param NamespaceInfo $nsInfo
202 */
203 public function __construct(
204 SpecialPageFactory $specialPageFactory,
205 RevisionLookup $revisionLookup,
206 $whitelistRead,
207 $whitelistReadRegexp,
208 $emailConfirmToEdit,
209 $blockDisablesLogin,
210 $groupPermissions,
211 $revokePermissions,
212 $availableRights,
213 NamespaceInfo $nsInfo
214 ) {
215 $this->specialPageFactory = $specialPageFactory;
216 $this->revisionLookup = $revisionLookup;
217 $this->whitelistRead = $whitelistRead;
218 $this->whitelistReadRegexp = $whitelistReadRegexp;
219 $this->emailConfirmToEdit = $emailConfirmToEdit;
220 $this->blockDisablesLogin = $blockDisablesLogin;
221 $this->groupPermissions = $groupPermissions;
222 $this->revokePermissions = $revokePermissions;
223 $this->availableRights = $availableRights;
224 $this->nsInfo = $nsInfo;
225 }
226
227 /**
228 * Can $user perform $action on a page?
229 *
230 * The method is intended to replace Title::userCan()
231 * The $user parameter need to be superseded by UserIdentity value in future
232 * The $title parameter need to be superseded by PageIdentity value in future
233 *
234 * @see Title::userCan()
235 *
236 * @param string $action
237 * @param User $user
238 * @param LinkTarget $page
239 * @param string $rigor One of PermissionManager::RIGOR_ constants
240 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
241 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
242 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
243 *
244 * @return bool
245 */
246 public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
247 return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
248 }
249
250 /**
251 * Can $user perform $action on a page?
252 *
253 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
254 *
255 * @param string $action Action that permission needs to be checked for
256 * @param User $user User to check
257 * @param LinkTarget $page
258 * @param string $rigor One of PermissionManager::RIGOR_ constants
259 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
260 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
261 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
262 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
263 * whose corresponding errors may be ignored.
264 *
265 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
266 */
267 public function getPermissionErrors(
268 $action,
269 User $user,
270 LinkTarget $page,
271 $rigor = self::RIGOR_SECURE,
272 $ignoreErrors = []
273 ) {
274 $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
275
276 // Remove the errors being ignored.
277 foreach ( $errors as $index => $error ) {
278 $errKey = is_array( $error ) ? $error[0] : $error;
279
280 if ( in_array( $errKey, $ignoreErrors ) ) {
281 unset( $errors[$index] );
282 }
283 if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
284 unset( $errors[$index] );
285 }
286 }
287
288 return $errors;
289 }
290
291 /**
292 * Check if user is blocked from editing a particular article. If the user does not
293 * have a block, this will return false.
294 *
295 * @param User $user
296 * @param LinkTarget $page Title to check
297 * @param bool $fromReplica Whether to check the replica DB instead of the master
298 *
299 * @return bool
300 */
301 public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
302 $block = $user->getBlock( $fromReplica );
303 if ( !$block ) {
304 return false;
305 }
306
307 // TODO: remove upon further migration to LinkTarget
308 $title = Title::newFromLinkTarget( $page );
309
310 $blocked = $user->isHidden();
311 if ( !$blocked ) {
312 // Special handling for a user's own talk page. The block is not aware
313 // of the user, so this must be done here.
314 if ( $title->equals( $user->getTalkPage() ) ) {
315 $blocked = $block->appliesToUsertalk( $title );
316 } else {
317 $blocked = $block->appliesToTitle( $title );
318 }
319 }
320
321 // only for the purpose of the hook. We really don't need this here.
322 $allowUsertalk = $user->isAllowUsertalk();
323
324 // Allow extensions to let a blocked user access a particular page
325 Hooks::run( 'UserIsBlockedFrom', [ $user, $title, &$blocked, &$allowUsertalk ] );
326
327 return $blocked;
328 }
329
330 /**
331 * Can $user perform $action on a page? This is an internal function,
332 * with multiple levels of checks depending on performance needs; see $rigor below.
333 * It does not check wfReadOnly().
334 *
335 * @param string $action Action that permission needs to be checked for
336 * @param User $user User to check
337 * @param LinkTarget $page
338 * @param string $rigor One of PermissionManager::RIGOR_ constants
339 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
340 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
341 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
342 * @param bool $short Set this to true to stop after the first permission error.
343 *
344 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
345 * @throws Exception
346 */
347 private function getPermissionErrorsInternal(
348 $action,
349 User $user,
350 LinkTarget $page,
351 $rigor = self::RIGOR_SECURE,
352 $short = false
353 ) {
354 if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
355 throw new Exception( "Invalid rigor parameter '$rigor'." );
356 }
357
358 # Read has special handling
359 if ( $action == 'read' ) {
360 $checks = [
361 'checkPermissionHooks',
362 'checkReadPermissions',
363 'checkUserBlock', // for wgBlockDisablesLogin
364 ];
365 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
366 # or checkUserConfigPermissions here as it will lead to duplicate
367 # error messages. This is okay to do since anywhere that checks for
368 # create will also check for edit, and those checks are called for edit.
369 } elseif ( $action == 'create' ) {
370 $checks = [
371 'checkQuickPermissions',
372 'checkPermissionHooks',
373 'checkPageRestrictions',
374 'checkCascadingSourcesRestrictions',
375 'checkActionPermissions',
376 'checkUserBlock'
377 ];
378 } else {
379 $checks = [
380 'checkQuickPermissions',
381 'checkPermissionHooks',
382 'checkSpecialsAndNSPermissions',
383 'checkSiteConfigPermissions',
384 'checkUserConfigPermissions',
385 'checkPageRestrictions',
386 'checkCascadingSourcesRestrictions',
387 'checkActionPermissions',
388 'checkUserBlock'
389 ];
390 }
391
392 $errors = [];
393 foreach ( $checks as $method ) {
394 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
395
396 if ( $short && $errors !== [] ) {
397 break;
398 }
399 }
400
401 return $errors;
402 }
403
404 /**
405 * Check various permission hooks
406 *
407 * @param string $action The action to check
408 * @param User $user User to check
409 * @param array $errors List of current errors
410 * @param string $rigor One of PermissionManager::RIGOR_ constants
411 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
412 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
413 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
414 * @param bool $short Short circuit on first error
415 *
416 * @param LinkTarget $page
417 *
418 * @return array List of errors
419 */
420 private function checkPermissionHooks(
421 $action,
422 User $user,
423 $errors,
424 $rigor,
425 $short,
426 LinkTarget $page
427 ) {
428 // TODO: remove when LinkTarget usage will expand further
429 $title = Title::newFromLinkTarget( $page );
430 // Use getUserPermissionsErrors instead
431 $result = '';
432 if ( !Hooks::run( 'userCan', [ &$title, &$user, $action, &$result ] ) ) {
433 return $result ? [] : [ [ 'badaccess-group0' ] ];
434 }
435 // Check getUserPermissionsErrors hook
436 if ( !Hooks::run( 'getUserPermissionsErrors', [ &$title, &$user, $action, &$result ] ) ) {
437 $errors = $this->resultToError( $errors, $result );
438 }
439 // Check getUserPermissionsErrorsExpensive hook
440 if (
441 $rigor !== self::RIGOR_QUICK
442 && !( $short && count( $errors ) > 0 )
443 && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$title, &$user, $action, &$result ] )
444 ) {
445 $errors = $this->resultToError( $errors, $result );
446 }
447
448 return $errors;
449 }
450
451 /**
452 * Add the resulting error code to the errors array
453 *
454 * @param array $errors List of current errors
455 * @param array|string|MessageSpecifier|false $result Result of errors
456 *
457 * @return array List of errors
458 */
459 private function resultToError( $errors, $result ) {
460 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
461 // A single array representing an error
462 $errors[] = $result;
463 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
464 // A nested array representing multiple errors
465 $errors = array_merge( $errors, $result );
466 } elseif ( $result !== '' && is_string( $result ) ) {
467 // A string representing a message-id
468 $errors[] = [ $result ];
469 } elseif ( $result instanceof MessageSpecifier ) {
470 // A message specifier representing an error
471 $errors[] = [ $result ];
472 } elseif ( $result === false ) {
473 // a generic "We don't want them to do that"
474 $errors[] = [ 'badaccess-group0' ];
475 }
476 return $errors;
477 }
478
479 /**
480 * Check that the user is allowed to read this page.
481 *
482 * @param string $action The action to check
483 * @param User $user User to check
484 * @param array $errors List of current errors
485 * @param string $rigor One of PermissionManager::RIGOR_ constants
486 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
487 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
488 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
489 * @param bool $short Short circuit on first error
490 *
491 * @param LinkTarget $page
492 *
493 * @return array List of errors
494 */
495 private function checkReadPermissions(
496 $action,
497 User $user,
498 $errors,
499 $rigor,
500 $short,
501 LinkTarget $page
502 ) {
503 // TODO: remove when LinkTarget usage will expand further
504 $title = Title::newFromLinkTarget( $page );
505
506 $whitelisted = false;
507 if ( $this->isEveryoneAllowed( 'read' ) ) {
508 # Shortcut for public wikis, allows skipping quite a bit of code
509 $whitelisted = true;
510 } elseif ( $this->userHasRight( $user, 'read' ) ) {
511 # If the user is allowed to read pages, he is allowed to read all pages
512 $whitelisted = true;
513 } elseif ( $this->isSameSpecialPage( 'Userlogin', $title )
514 || $this->isSameSpecialPage( 'PasswordReset', $title )
515 || $this->isSameSpecialPage( 'Userlogout', $title )
516 ) {
517 # Always grant access to the login page.
518 # Even anons need to be able to log in.
519 $whitelisted = true;
520 } elseif ( is_array( $this->whitelistRead ) && count( $this->whitelistRead ) ) {
521 # Time to check the whitelist
522 # Only do these checks is there's something to check against
523 $name = $title->getPrefixedText();
524 $dbName = $title->getPrefixedDBkey();
525
526 // Check for explicit whitelisting with and without underscores
527 if ( in_array( $name, $this->whitelistRead, true )
528 || in_array( $dbName, $this->whitelistRead, true ) ) {
529 $whitelisted = true;
530 } elseif ( $title->getNamespace() == NS_MAIN ) {
531 # Old settings might have the title prefixed with
532 # a colon for main-namespace pages
533 if ( in_array( ':' . $name, $this->whitelistRead ) ) {
534 $whitelisted = true;
535 }
536 } elseif ( $title->isSpecialPage() ) {
537 # If it's a special page, ditch the subpage bit and check again
538 $name = $title->getDBkey();
539 list( $name, /* $subpage */ ) =
540 $this->specialPageFactory->resolveAlias( $name );
541 if ( $name ) {
542 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
543 if ( in_array( $pure, $this->whitelistRead, true ) ) {
544 $whitelisted = true;
545 }
546 }
547 }
548 }
549
550 if ( !$whitelisted && is_array( $this->whitelistReadRegexp )
551 && !empty( $this->whitelistReadRegexp ) ) {
552 $name = $title->getPrefixedText();
553 // Check for regex whitelisting
554 foreach ( $this->whitelistReadRegexp as $listItem ) {
555 if ( preg_match( $listItem, $name ) ) {
556 $whitelisted = true;
557 break;
558 }
559 }
560 }
561
562 if ( !$whitelisted ) {
563 # If the title is not whitelisted, give extensions a chance to do so...
564 Hooks::run( 'TitleReadWhitelist', [ $title, $user, &$whitelisted ] );
565 if ( !$whitelisted ) {
566 $errors[] = $this->missingPermissionError( $action, $short );
567 }
568 }
569
570 return $errors;
571 }
572
573 /**
574 * Get a description array when the user doesn't have the right to perform
575 * $action (i.e. when User::isAllowed() returns false)
576 *
577 * @param string $action The action to check
578 * @param bool $short Short circuit on first error
579 * @return array Array containing an error message key and any parameters
580 */
581 private function missingPermissionError( $action, $short ) {
582 // We avoid expensive display logic for quickUserCan's and such
583 if ( $short ) {
584 return [ 'badaccess-group0' ];
585 }
586
587 // TODO: it would be a good idea to replace the method below with something else like
588 // maybe callback injection
589 return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
590 }
591
592 /**
593 * Returns true if this title resolves to the named special page
594 *
595 * @param string $name The special page name
596 * @param LinkTarget $page
597 *
598 * @return bool
599 */
600 private function isSameSpecialPage( $name, LinkTarget $page ) {
601 if ( $page->getNamespace() == NS_SPECIAL ) {
602 list( $thisName, /* $subpage */ ) =
603 $this->specialPageFactory->resolveAlias( $page->getDBkey() );
604 if ( $name == $thisName ) {
605 return true;
606 }
607 }
608 return false;
609 }
610
611 /**
612 * Check that the user isn't blocked from editing.
613 *
614 * @param string $action The action to check
615 * @param User $user User to check
616 * @param array $errors List of current errors
617 * @param string $rigor One of PermissionManager::RIGOR_ constants
618 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
619 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
620 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
621 * @param bool $short Short circuit on first error
622 *
623 * @param LinkTarget $page
624 *
625 * @return array List of errors
626 */
627 private function checkUserBlock(
628 $action,
629 User $user,
630 $errors,
631 $rigor,
632 $short,
633 LinkTarget $page
634 ) {
635 // Account creation blocks handled at userlogin.
636 // Unblocking handled in SpecialUnblock
637 if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
638 return $errors;
639 }
640
641 // Optimize for a very common case
642 if ( $action === 'read' && !$this->blockDisablesLogin ) {
643 return $errors;
644 }
645
646 if ( $this->emailConfirmToEdit
647 && !$user->isEmailConfirmed()
648 && $action === 'edit'
649 ) {
650 $errors[] = [ 'confirmedittext' ];
651 }
652
653 $useReplica = ( $rigor !== self::RIGOR_SECURE );
654 $block = $user->getBlock( $useReplica );
655
656 // If the user does not have a block, or the block they do have explicitly
657 // allows the action (like "read" or "upload").
658 if ( !$block || $block->appliesToRight( $action ) === false ) {
659 return $errors;
660 }
661
662 // Determine if the user is blocked from this action on this page.
663 // What gets passed into this method is a user right, not an action name.
664 // There is no way to instantiate an action by restriction. However, this
665 // will get the action where the restriction is the same. This may result
666 // in actions being blocked that shouldn't be.
667 $actionObj = null;
668 if ( Action::exists( $action ) ) {
669 // TODO: this drags a ton of dependencies in, would be good to avoid WikiPage
670 // instantiation and decouple it creating an ActionPermissionChecker interface
671 $wikiPage = WikiPage::factory( Title::newFromLinkTarget( $page, 'clone' ) );
672 // Creating an action will perform several database queries to ensure that
673 // the action has not been overridden by the content type.
674 // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
675 // probably we may use fake context object since it's unlikely that Action uses it
676 // anyway. It would be nice if we could avoid instantiating the Action at all.
677 $actionObj = Action::factory( $action, $wikiPage, RequestContext::getMain() );
678 // Ensure that the retrieved action matches the restriction.
679 if ( $actionObj && $actionObj->getRestriction() !== $action ) {
680 $actionObj = null;
681 }
682 }
683
684 // If no action object is returned, assume that the action requires unblock
685 // which is the default.
686 if ( !$actionObj || $actionObj->requiresUnblock() ) {
687 if ( $this->isBlockedFrom( $user, $page, $useReplica ) ) {
688 // @todo FIXME: Pass the relevant context into this function.
689 $errors[] = $block->getPermissionsError( RequestContext::getMain() );
690 }
691 }
692
693 return $errors;
694 }
695
696 /**
697 * Permissions checks that fail most often, and which are easiest to test.
698 *
699 * @param string $action The action to check
700 * @param User $user User to check
701 * @param array $errors List of current errors
702 * @param string $rigor One of PermissionManager::RIGOR_ constants
703 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
704 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
705 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
706 * @param bool $short Short circuit on first error
707 *
708 * @param LinkTarget $page
709 *
710 * @return array List of errors
711 */
712 private function checkQuickPermissions(
713 $action,
714 User $user,
715 $errors,
716 $rigor,
717 $short,
718 LinkTarget $page
719 ) {
720 // TODO: remove when LinkTarget usage will expand further
721 $title = Title::newFromLinkTarget( $page );
722
723 if ( !Hooks::run( 'TitleQuickPermissions',
724 [ $title, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
725 ) {
726 return $errors;
727 }
728
729 $isSubPage = $this->nsInfo->hasSubpages( $title->getNamespace() ) ?
730 strpos( $title->getText(), '/' ) !== false : false;
731
732 if ( $action == 'create' ) {
733 if (
734 ( $this->nsInfo->isTalk( $title->getNamespace() ) &&
735 !$this->userHasRight( $user, 'createtalk' ) ) ||
736 ( !$this->nsInfo->isTalk( $title->getNamespace() ) &&
737 !$this->userHasRight( $user, 'createpage' ) )
738 ) {
739 $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
740 }
741 } elseif ( $action == 'move' ) {
742 if ( !$this->userHasRight( $user, 'move-rootuserpages' )
743 && $title->getNamespace() == NS_USER && !$isSubPage ) {
744 // Show user page-specific message only if the user can move other pages
745 $errors[] = [ 'cant-move-user-page' ];
746 }
747
748 // Check if user is allowed to move files if it's a file
749 if ( $title->getNamespace() == NS_FILE &&
750 !$this->userHasRight( $user, 'movefile' ) ) {
751 $errors[] = [ 'movenotallowedfile' ];
752 }
753
754 // Check if user is allowed to move category pages if it's a category page
755 if ( $title->getNamespace() == NS_CATEGORY &&
756 !$this->userHasRight( $user, 'move-categorypages' ) ) {
757 $errors[] = [ 'cant-move-category-page' ];
758 }
759
760 if ( !$this->userHasRight( $user, 'move' ) ) {
761 // User can't move anything
762 $userCanMove = $this->groupHasPermission( 'user', 'move' );
763 $autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' );
764 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
765 // custom message if logged-in users without any special rights can move
766 $errors[] = [ 'movenologintext' ];
767 } else {
768 $errors[] = [ 'movenotallowed' ];
769 }
770 }
771 } elseif ( $action == 'move-target' ) {
772 if ( !$this->userHasRight( $user, 'move' ) ) {
773 // User can't move anything
774 $errors[] = [ 'movenotallowed' ];
775 } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' )
776 && $title->getNamespace() == NS_USER && !$isSubPage ) {
777 // Show user page-specific message only if the user can move other pages
778 $errors[] = [ 'cant-move-to-user-page' ];
779 } elseif ( !$this->userHasRight( $user, 'move-categorypages' )
780 && $title->getNamespace() == NS_CATEGORY ) {
781 // Show category page-specific message only if the user can move other pages
782 $errors[] = [ 'cant-move-to-category-page' ];
783 }
784 } elseif ( !$this->userHasRight( $user, $action ) ) {
785 $errors[] = $this->missingPermissionError( $action, $short );
786 }
787
788 return $errors;
789 }
790
791 /**
792 * Check against page_restrictions table requirements on this
793 * page. The user must possess all required rights for this
794 * action.
795 *
796 * @param string $action The action to check
797 * @param User $user User to check
798 * @param array $errors List of current errors
799 * @param string $rigor One of PermissionManager::RIGOR_ constants
800 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
801 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
802 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
803 * @param bool $short Short circuit on first error
804 *
805 * @param LinkTarget $page
806 *
807 * @return array List of errors
808 */
809 private function checkPageRestrictions(
810 $action,
811 User $user,
812 $errors,
813 $rigor,
814 $short,
815 LinkTarget $page
816 ) {
817 // TODO: remove & rework upon further use of LinkTarget
818 $title = Title::newFromLinkTarget( $page );
819 foreach ( $title->getRestrictions( $action ) as $right ) {
820 // Backwards compatibility, rewrite sysop -> editprotected
821 if ( $right == 'sysop' ) {
822 $right = 'editprotected';
823 }
824 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
825 if ( $right == 'autoconfirmed' ) {
826 $right = 'editsemiprotected';
827 }
828 if ( $right == '' ) {
829 continue;
830 }
831 if ( !$this->userHasRight( $user, $right ) ) {
832 $errors[] = [ 'protectedpagetext', $right, $action ];
833 } elseif ( $title->areRestrictionsCascading() &&
834 !$this->userHasRight( $user, 'protect' ) ) {
835 $errors[] = [ 'protectedpagetext', 'protect', $action ];
836 }
837 }
838
839 return $errors;
840 }
841
842 /**
843 * Check restrictions on cascading pages.
844 *
845 * @param string $action The action to check
846 * @param User $user User to check
847 * @param array $errors List of current errors
848 * @param string $rigor One of PermissionManager::RIGOR_ constants
849 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
850 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
851 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
852 * @param bool $short Short circuit on first error
853 *
854 * @param LinkTarget $page
855 *
856 * @return array List of errors
857 */
858 private function checkCascadingSourcesRestrictions(
859 $action,
860 User $user,
861 $errors,
862 $rigor,
863 $short,
864 LinkTarget $page
865 ) {
866 // TODO: remove & rework upon further use of LinkTarget
867 $title = Title::newFromLinkTarget( $page );
868 if ( $rigor !== self::RIGOR_QUICK && !$title->isUserConfigPage() ) {
869 # We /could/ use the protection level on the source page, but it's
870 # fairly ugly as we have to establish a precedence hierarchy for pages
871 # included by multiple cascade-protected pages. So just restrict
872 # it to people with 'protect' permission, as they could remove the
873 # protection anyway.
874 list( $cascadingSources, $restrictions ) = $title->getCascadeProtectionSources();
875 # Cascading protection depends on more than this page...
876 # Several cascading protected pages may include this page...
877 # Check each cascading level
878 # This is only for protection restrictions, not for all actions
879 if ( isset( $restrictions[$action] ) ) {
880 foreach ( $restrictions[$action] as $right ) {
881 // Backwards compatibility, rewrite sysop -> editprotected
882 if ( $right == 'sysop' ) {
883 $right = 'editprotected';
884 }
885 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
886 if ( $right == 'autoconfirmed' ) {
887 $right = 'editsemiprotected';
888 }
889 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
890 $wikiPages = '';
891 /** @var Title $wikiPage */
892 foreach ( $cascadingSources as $wikiPage ) {
893 $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
894 }
895 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
896 }
897 }
898 }
899 }
900
901 return $errors;
902 }
903
904 /**
905 * Check action permissions not already checked in checkQuickPermissions
906 *
907 * @param string $action The action to check
908 * @param User $user User to check
909 * @param array $errors List of current errors
910 * @param string $rigor One of PermissionManager::RIGOR_ constants
911 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
912 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
913 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
914 * @param bool $short Short circuit on first error
915 *
916 * @param LinkTarget $page
917 *
918 * @return array List of errors
919 */
920 private function checkActionPermissions(
921 $action,
922 User $user,
923 $errors,
924 $rigor,
925 $short,
926 LinkTarget $page
927 ) {
928 global $wgDeleteRevisionsLimit, $wgLang;
929
930 // TODO: remove & rework upon further use of LinkTarget
931 $title = Title::newFromLinkTarget( $page );
932
933 if ( $action == 'protect' ) {
934 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
935 // If they can't edit, they shouldn't protect.
936 $errors[] = [ 'protect-cantedit' ];
937 }
938 } elseif ( $action == 'create' ) {
939 $title_protection = $title->getTitleProtection();
940 if ( $title_protection ) {
941 if ( $title_protection['permission'] == ''
942 || !$this->userHasRight( $user, $title_protection['permission'] )
943 ) {
944 $errors[] = [
945 'titleprotected',
946 // TODO: get rid of the User dependency
947 User::whoIs( $title_protection['user'] ),
948 $title_protection['reason']
949 ];
950 }
951 }
952 } elseif ( $action == 'move' ) {
953 // Check for immobile pages
954 if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
955 // Specific message for this case
956 $errors[] = [ 'immobile-source-namespace', $title->getNsText() ];
957 } elseif ( !$title->isMovable() ) {
958 // Less specific message for rarer cases
959 $errors[] = [ 'immobile-source-page' ];
960 }
961 } elseif ( $action == 'move-target' ) {
962 if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
963 $errors[] = [ 'immobile-target-namespace', $title->getNsText() ];
964 } elseif ( !$title->isMovable() ) {
965 $errors[] = [ 'immobile-target-page' ];
966 }
967 } elseif ( $action == 'delete' ) {
968 $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $title );
969 if ( !$tempErrors ) {
970 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
971 $user, $tempErrors, $rigor, true, $title );
972 }
973 if ( $tempErrors ) {
974 // If protection keeps them from editing, they shouldn't be able to delete.
975 $errors[] = [ 'deleteprotected' ];
976 }
977 if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit
978 && !$this->userCan( 'bigdelete', $user, $title ) && $title->isBigDeletion()
979 ) {
980 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
981 }
982 } elseif ( $action === 'undelete' ) {
983 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
984 // Undeleting implies editing
985 $errors[] = [ 'undelete-cantedit' ];
986 }
987 if ( !$title->exists()
988 && count( $this->getPermissionErrorsInternal( 'create', $user, $title, $rigor, true ) )
989 ) {
990 // Undeleting where nothing currently exists implies creating
991 $errors[] = [ 'undelete-cantcreate' ];
992 }
993 }
994 return $errors;
995 }
996
997 /**
998 * Check permissions on special pages & namespaces
999 *
1000 * @param string $action The action to check
1001 * @param User $user User to check
1002 * @param array $errors List of current errors
1003 * @param string $rigor One of PermissionManager::RIGOR_ constants
1004 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1005 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1006 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1007 * @param bool $short Short circuit on first error
1008 *
1009 * @param LinkTarget $page
1010 *
1011 * @return array List of errors
1012 */
1013 private function checkSpecialsAndNSPermissions(
1014 $action,
1015 User $user,
1016 $errors,
1017 $rigor,
1018 $short,
1019 LinkTarget $page
1020 ) {
1021 // TODO: remove & rework upon further use of LinkTarget
1022 $title = Title::newFromLinkTarget( $page );
1023
1024 # Only 'createaccount' can be performed on special pages,
1025 # which don't actually exist in the DB.
1026 if ( $title->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) {
1027 $errors[] = [ 'ns-specialprotected' ];
1028 }
1029
1030 # Check $wgNamespaceProtection for restricted namespaces
1031 if ( $title->isNamespaceProtected( $user ) ) {
1032 $ns = $title->getNamespace() == NS_MAIN ?
1033 wfMessage( 'nstab-main' )->text() : $title->getNsText();
1034 $errors[] = $title->getNamespace() == NS_MEDIAWIKI ?
1035 [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
1036 }
1037
1038 return $errors;
1039 }
1040
1041 /**
1042 * Check sitewide CSS/JSON/JS permissions
1043 *
1044 * @param string $action The action to check
1045 * @param User $user User to check
1046 * @param array $errors List of current errors
1047 * @param string $rigor One of PermissionManager::RIGOR_ constants
1048 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1049 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1050 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1051 * @param bool $short Short circuit on first error
1052 *
1053 * @param LinkTarget $page
1054 *
1055 * @return array List of errors
1056 */
1057 private function checkSiteConfigPermissions(
1058 $action,
1059 User $user,
1060 $errors,
1061 $rigor,
1062 $short,
1063 LinkTarget $page
1064 ) {
1065 // TODO: remove & rework upon further use of LinkTarget
1066 $title = Title::newFromLinkTarget( $page );
1067
1068 if ( $action != 'patrol' ) {
1069 $error = null;
1070 // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
1071 // editinterface right. That's implemented as a restriction so no check needed here.
1072 if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) {
1073 $error = [ 'sitecssprotected', $action ];
1074 } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) {
1075 $error = [ 'sitejsonprotected', $action ];
1076 } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) {
1077 $error = [ 'sitejsprotected', $action ];
1078 } elseif ( $title->isRawHtmlMessage() ) {
1079 // Raw HTML can be used to deploy CSS or JS so require rights for both.
1080 if ( !$this->userHasRight( $user, 'editsitejs' ) ) {
1081 $error = [ 'sitejsprotected', $action ];
1082 } elseif ( !$this->userHasRight( $user, 'editsitecss' ) ) {
1083 $error = [ 'sitecssprotected', $action ];
1084 }
1085 }
1086
1087 if ( $error ) {
1088 if ( $this->userHasRight( $user, 'editinterface' ) ) {
1089 // Most users / site admins will probably find out about the new, more restrictive
1090 // permissions by failing to edit something. Give them more info.
1091 // TODO remove this a few release cycles after 1.32
1092 $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
1093 }
1094 $errors[] = $error;
1095 }
1096 }
1097
1098 return $errors;
1099 }
1100
1101 /**
1102 * Check CSS/JSON/JS sub-page permissions
1103 *
1104 * @param string $action The action to check
1105 * @param User $user User to check
1106 * @param array $errors List of current errors
1107 * @param string $rigor One of PermissionManager::RIGOR_ constants
1108 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1109 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1110 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1111 * @param bool $short Short circuit on first error
1112 *
1113 * @param LinkTarget $page
1114 *
1115 * @return array List of errors
1116 */
1117 private function checkUserConfigPermissions(
1118 $action,
1119 User $user,
1120 $errors,
1121 $rigor,
1122 $short,
1123 LinkTarget $page
1124 ) {
1125 // TODO: remove & rework upon further use of LinkTarget
1126 $title = Title::newFromLinkTarget( $page );
1127
1128 # Protect css/json/js subpages of user pages
1129 # XXX: this might be better using restrictions
1130
1131 if ( $action === 'patrol' ) {
1132 return $errors;
1133 }
1134
1135 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) {
1136 // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1137 if (
1138 $title->isUserCssConfigPage()
1139 && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
1140 ) {
1141 $errors[] = [ 'mycustomcssprotected', $action ];
1142 } elseif (
1143 $title->isUserJsonConfigPage()
1144 && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
1145 ) {
1146 $errors[] = [ 'mycustomjsonprotected', $action ];
1147 } elseif (
1148 $title->isUserJsConfigPage()
1149 && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
1150 ) {
1151 $errors[] = [ 'mycustomjsprotected', $action ];
1152 } elseif (
1153 $title->isUserJsConfigPage()
1154 && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' )
1155 ) {
1156 // T207750 - do not allow users to edit a redirect if they couldn't edit the target
1157 $rev = $this->revisionLookup->getRevisionByTitle( $title );
1158 $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null;
1159 $target = $content ? $content->getUltimateRedirectTarget() : null;
1160 if ( $target && (
1161 !$target->inNamespace( NS_USER )
1162 || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
1163 ) ) {
1164 $errors[] = [ 'mycustomjsredirectprotected', $action ];
1165 }
1166 }
1167 } else {
1168 // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
1169 // deletion/suppression which cannot be used for attacks and we want to avoid the
1170 // situation where an unprivileged user can post abusive content on their subpages
1171 // and only very highly privileged users could remove it.
1172 if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
1173 if (
1174 $title->isUserCssConfigPage()
1175 && !$this->userHasRight( $user, 'editusercss' )
1176 ) {
1177 $errors[] = [ 'customcssprotected', $action ];
1178 } elseif (
1179 $title->isUserJsonConfigPage()
1180 && !$this->userHasRight( $user, 'edituserjson' )
1181 ) {
1182 $errors[] = [ 'customjsonprotected', $action ];
1183 } elseif (
1184 $title->isUserJsConfigPage()
1185 && !$this->userHasRight( $user, 'edituserjs' )
1186 ) {
1187 $errors[] = [ 'customjsprotected', $action ];
1188 }
1189 }
1190 }
1191
1192 return $errors;
1193 }
1194
1195 /**
1196 * Testing a permission
1197 *
1198 * @since 1.34
1199 *
1200 * @param UserIdentity $user
1201 * @param string $action
1202 *
1203 * @return bool
1204 */
1205 public function userHasRight( UserIdentity $user, $action = '' ) {
1206 if ( $action === '' ) {
1207 return true; // In the spirit of DWIM
1208 }
1209 // Use strict parameter to avoid matching numeric 0 accidentally inserted
1210 // by misconfiguration: 0 == 'foo'
1211 return in_array( $action, $this->getUserPermissions( $user ), true );
1212 }
1213
1214 /**
1215 * Get the permissions this user has.
1216 *
1217 * @since 1.34
1218 *
1219 * @param UserIdentity $user
1220 *
1221 * @return string[] permission names
1222 */
1223 public function getUserPermissions( UserIdentity $user ) {
1224 $user = User::newFromIdentity( $user );
1225 if ( !isset( $this->usersRights[ $user->getId() ] ) ) {
1226 $this->usersRights[ $user->getId() ] = $this->getGroupPermissions(
1227 $user->getEffectiveGroups()
1228 );
1229 Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] );
1230
1231 // Deny any rights denied by the user's session, unless this
1232 // endpoint has no sessions.
1233 if ( !defined( 'MW_NO_SESSION' ) ) {
1234 // FIXME: $user->getRequest().. need to be replaced with something else
1235 $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
1236 if ( $allowedRights !== null ) {
1237 $this->usersRights[ $user->getId() ] = array_intersect(
1238 $this->usersRights[ $user->getId() ],
1239 $allowedRights
1240 );
1241 }
1242 }
1243
1244 Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] );
1245 // Force reindexation of rights when a hook has unset one of them
1246 $this->usersRights[ $user->getId() ] = array_values(
1247 array_unique( $this->usersRights[ $user->getId() ] )
1248 );
1249
1250 if (
1251 $user->isLoggedIn() &&
1252 $this->blockDisablesLogin &&
1253 $user->getBlock()
1254 ) {
1255 $anon = new User;
1256 $this->usersRights[ $user->getId() ] = array_intersect(
1257 $this->usersRights[ $user->getId() ],
1258 $this->getUserPermissions( $anon )
1259 );
1260 }
1261 }
1262 $rights = $this->usersRights[ $user->getId() ];
1263 foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) {
1264 $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1265 }
1266 return $rights;
1267 }
1268
1269 /**
1270 * Clears users permissions cache, if specific user is provided it tries to clear
1271 * permissions cache only for provided user.
1272 *
1273 * @since 1.34
1274 *
1275 * @param User|null $user
1276 */
1277 public function invalidateUsersRightsCache( $user = null ) {
1278 if ( $user !== null ) {
1279 if ( isset( $this->usersRights[ $user->getId() ] ) ) {
1280 unset( $this->usersRights[$user->getId()] );
1281 }
1282 } else {
1283 $this->usersRights = null;
1284 }
1285 }
1286
1287 /**
1288 * Check, if the given group has the given permission
1289 *
1290 * If you're wanting to check whether all users have a permission, use
1291 * PermissionManager::isEveryoneAllowed() instead. That properly checks if it's revoked
1292 * from anyone.
1293 *
1294 * @since 1.34
1295 *
1296 * @param string $group Group to check
1297 * @param string $role Role to check
1298 *
1299 * @return bool
1300 */
1301 public function groupHasPermission( $group, $role ) {
1302 return isset( $this->groupPermissions[$group][$role] ) &&
1303 $this->groupPermissions[$group][$role] &&
1304 !( isset( $this->revokePermissions[$group][$role] ) &&
1305 $this->revokePermissions[$group][$role] );
1306 }
1307
1308 /**
1309 * Get the permissions associated with a given list of groups
1310 *
1311 * @since 1.34
1312 *
1313 * @param array $groups Array of Strings List of internal group names
1314 * @return array Array of Strings List of permission key names for given groups combined
1315 */
1316 public function getGroupPermissions( $groups ) {
1317 $rights = [];
1318 // grant every granted permission first
1319 foreach ( $groups as $group ) {
1320 if ( isset( $this->groupPermissions[$group] ) ) {
1321 $rights = array_merge( $rights,
1322 // array_filter removes empty items
1323 array_keys( array_filter( $this->groupPermissions[$group] ) ) );
1324 }
1325 }
1326 // now revoke the revoked permissions
1327 foreach ( $groups as $group ) {
1328 if ( isset( $this->revokePermissions[$group] ) ) {
1329 $rights = array_diff( $rights,
1330 array_keys( array_filter( $this->revokePermissions[$group] ) ) );
1331 }
1332 }
1333 return array_unique( $rights );
1334 }
1335
1336 /**
1337 * Get all the groups who have a given permission
1338 *
1339 * @since 1.34
1340 *
1341 * @param string $role Role to check
1342 * @return array Array of Strings List of internal group names with the given permission
1343 */
1344 public function getGroupsWithPermission( $role ) {
1345 $allowedGroups = [];
1346 foreach ( array_keys( $this->groupPermissions ) as $group ) {
1347 if ( $this->groupHasPermission( $group, $role ) ) {
1348 $allowedGroups[] = $group;
1349 }
1350 }
1351 return $allowedGroups;
1352 }
1353
1354 /**
1355 * Check if all users may be assumed to have the given permission
1356 *
1357 * We generally assume so if the right is granted to '*' and isn't revoked
1358 * on any group. It doesn't attempt to take grants or other extension
1359 * limitations on rights into account in the general case, though, as that
1360 * would require it to always return false and defeat the purpose.
1361 * Specifically, session-based rights restrictions (such as OAuth or bot
1362 * passwords) are applied based on the current session.
1363 *
1364 * @param string $right Right to check
1365 *
1366 * @return bool
1367 * @since 1.34
1368 */
1369 public function isEveryoneAllowed( $right ) {
1370 // Use the cached results, except in unit tests which rely on
1371 // being able change the permission mid-request
1372 if ( isset( $this->cachedRights[$right] ) ) {
1373 return $this->cachedRights[$right];
1374 }
1375
1376 if ( !isset( $this->groupPermissions['*'][$right] )
1377 || !$this->groupPermissions['*'][$right] ) {
1378 $this->cachedRights[$right] = false;
1379 return false;
1380 }
1381
1382 // If it's revoked anywhere, then everyone doesn't have it
1383 foreach ( $this->revokePermissions as $rights ) {
1384 if ( isset( $rights[$right] ) && $rights[$right] ) {
1385 $this->cachedRights[$right] = false;
1386 return false;
1387 }
1388 }
1389
1390 // Remove any rights that aren't allowed to the global-session user,
1391 // unless there are no sessions for this endpoint.
1392 if ( !defined( 'MW_NO_SESSION' ) ) {
1393
1394 // XXX: think what could be done with the below
1395 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
1396 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
1397 $this->cachedRights[$right] = false;
1398 return false;
1399 }
1400 }
1401
1402 // Allow extensions to say false
1403 if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
1404 $this->cachedRights[$right] = false;
1405 return false;
1406 }
1407
1408 $this->cachedRights[$right] = true;
1409 return true;
1410 }
1411
1412 /**
1413 * Get a list of all available permissions.
1414 *
1415 * @since 1.34
1416 *
1417 * @return string[] Array of permission names
1418 */
1419 public function getAllPermissions() {
1420 if ( $this->allRights === false ) {
1421 if ( count( $this->availableRights ) ) {
1422 $this->allRights = array_unique( array_merge(
1423 $this->coreRights,
1424 $this->availableRights
1425 ) );
1426 } else {
1427 $this->allRights = $this->coreRights;
1428 }
1429 Hooks::run( 'UserGetAllRights', [ &$this->allRights ] );
1430 }
1431 return $this->allRights;
1432 }
1433
1434 /**
1435 * Add temporary user rights, only valid for the current scope.
1436 * This is meant for making it possible to programatically trigger certain actions that
1437 * the user wouldn't be able to trigger themselves; e.g. allow users without the bot right
1438 * to make bot-flagged actions through certain special pages.
1439 * Returns a "scope guard" variable; whenever that variable goes out of scope or is consumed
1440 * via ScopedCallback::consume(), the temporary rights are revoked.
1441 *
1442 * @since 1.34
1443 *
1444 * @param UserIdentity $user
1445 * @param string|string[] $rights
1446 * @return ScopedCallback
1447 */
1448 public function addTemporaryUserRights( UserIdentity $user, $rights ) {
1449 $userId = $user->getId();
1450 $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
1451 $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
1452 return new ScopedCallback( function () use ( $userId, $nextKey ) {
1453 unset( $this->temporaryUserRights[$userId][$nextKey] );
1454 } );
1455 }
1456
1457 /**
1458 * Overrides user permissions cache
1459 *
1460 * @since 1.34
1461 *
1462 * @param User $user
1463 * @param string[]|string $rights
1464 *
1465 * @throws Exception
1466 */
1467 public function overrideUserRightsForTesting( $user, $rights = [] ) {
1468 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1469 throw new Exception( __METHOD__ . ' can not be called outside of tests' );
1470 }
1471 $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ];
1472 }
1473
1474 }