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