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