Merge "REST: Use ob_clean to wipe display_errors output"
[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 $title = 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 ( $title->equals( $user->getTalkPage() ) ) {
312 $blocked = $block->appliesToUsertalk( $title );
313 } else {
314 $blocked = $block->appliesToTitle( $title );
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, $title, &$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 $title = Title::newFromLinkTarget( $page );
427 // Use getUserPermissionsErrors instead
428 $result = '';
429 if ( !Hooks::run( 'userCan', [ &$title, &$user, $action, &$result ] ) ) {
430 return $result ? [] : [ [ 'badaccess-group0' ] ];
431 }
432 // Check getUserPermissionsErrors hook
433 if ( !Hooks::run( 'getUserPermissionsErrors', [ &$title, &$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', [ &$title, &$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 $title = Title::newFromLinkTarget( $page );
502
503 $whitelisted = false;
504 if ( $this->isEveryoneAllowed( 'read' ) ) {
505 # Shortcut for public wikis, allows skipping quite a bit of code
506 $whitelisted = true;
507 } elseif ( $this->userHasRight( $user, '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', $title )
511 || $this->isSameSpecialPage( 'PasswordReset', $title )
512 || $this->isSameSpecialPage( 'Userlogout', $title )
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 = $title->getPrefixedText();
521 $dbName = $title->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 ( $title->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 ( $title->isSpecialPage() ) {
534 # If it's a special page, ditch the subpage bit and check again
535 $name = $title->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 = $title->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', [ $title, $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 $title = Title::newFromLinkTarget( $page );
719
720 if ( !Hooks::run( 'TitleQuickPermissions',
721 [ $title, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
722 ) {
723 return $errors;
724 }
725
726 $isSubPage = $this->nsInfo->hasSubpages( $title->getNamespace() ) ?
727 strpos( $title->getText(), '/' ) !== false : false;
728
729 if ( $action == 'create' ) {
730 if (
731 ( $this->nsInfo->isTalk( $title->getNamespace() ) &&
732 !$this->userHasRight( $user, 'createtalk' ) ) ||
733 ( !$this->nsInfo->isTalk( $title->getNamespace() ) &&
734 !$this->userHasRight( $user, 'createpage' ) )
735 ) {
736 $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
737 }
738 } elseif ( $action == 'move' ) {
739 if ( !$this->userHasRight( $user, 'move-rootuserpages' )
740 && $title->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 ( $title->getNamespace() == NS_FILE &&
747 !$this->userHasRight( $user, 'movefile' ) ) {
748 $errors[] = [ 'movenotallowedfile' ];
749 }
750
751 // Check if user is allowed to move category pages if it's a category page
752 if ( $title->getNamespace() == NS_CATEGORY &&
753 !$this->userHasRight( $user, 'move-categorypages' ) ) {
754 $errors[] = [ 'cant-move-category-page' ];
755 }
756
757 if ( !$this->userHasRight( $user, 'move' ) ) {
758 // User can't move anything
759 $userCanMove = $this->groupHasPermission( 'user', 'move' );
760 $autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' );
761 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
762 // custom message if logged-in users without any special rights can move
763 $errors[] = [ 'movenologintext' ];
764 } else {
765 $errors[] = [ 'movenotallowed' ];
766 }
767 }
768 } elseif ( $action == 'move-target' ) {
769 if ( !$this->userHasRight( $user, 'move' ) ) {
770 // User can't move anything
771 $errors[] = [ 'movenotallowed' ];
772 } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' )
773 && $title->getNamespace() == NS_USER && !$isSubPage ) {
774 // Show user page-specific message only if the user can move other pages
775 $errors[] = [ 'cant-move-to-user-page' ];
776 } elseif ( !$this->userHasRight( $user, 'move-categorypages' )
777 && $title->getNamespace() == NS_CATEGORY ) {
778 // Show category page-specific message only if the user can move other pages
779 $errors[] = [ 'cant-move-to-category-page' ];
780 }
781 } elseif ( !$this->userHasRight( $user, $action ) ) {
782 $errors[] = $this->missingPermissionError( $action, $short );
783 }
784
785 return $errors;
786 }
787
788 /**
789 * Check against page_restrictions table requirements on this
790 * page. The user must possess all required rights for this
791 * action.
792 *
793 * @param string $action The action to check
794 * @param User $user User to check
795 * @param array $errors List of current errors
796 * @param string $rigor One of PermissionManager::RIGOR_ constants
797 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
798 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
799 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
800 * @param bool $short Short circuit on first error
801 *
802 * @param LinkTarget $page
803 *
804 * @return array List of errors
805 */
806 private function checkPageRestrictions(
807 $action,
808 User $user,
809 $errors,
810 $rigor,
811 $short,
812 LinkTarget $page
813 ) {
814 // TODO: remove & rework upon further use of LinkTarget
815 $title = Title::newFromLinkTarget( $page );
816 foreach ( $title->getRestrictions( $action ) as $right ) {
817 // Backwards compatibility, rewrite sysop -> editprotected
818 if ( $right == 'sysop' ) {
819 $right = 'editprotected';
820 }
821 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
822 if ( $right == 'autoconfirmed' ) {
823 $right = 'editsemiprotected';
824 }
825 if ( $right == '' ) {
826 continue;
827 }
828 if ( !$this->userHasRight( $user, $right ) ) {
829 $errors[] = [ 'protectedpagetext', $right, $action ];
830 } elseif ( $title->areRestrictionsCascading() &&
831 !$this->userHasRight( $user, 'protect' ) ) {
832 $errors[] = [ 'protectedpagetext', 'protect', $action ];
833 }
834 }
835
836 return $errors;
837 }
838
839 /**
840 * Check restrictions on cascading pages.
841 *
842 * @param string $action The action to check
843 * @param User $user User to check
844 * @param array $errors List of current errors
845 * @param string $rigor One of PermissionManager::RIGOR_ constants
846 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
847 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
848 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
849 * @param bool $short Short circuit on first error
850 *
851 * @param LinkTarget $page
852 *
853 * @return array List of errors
854 */
855 private function checkCascadingSourcesRestrictions(
856 $action,
857 User $user,
858 $errors,
859 $rigor,
860 $short,
861 LinkTarget $page
862 ) {
863 // TODO: remove & rework upon further use of LinkTarget
864 $title = Title::newFromLinkTarget( $page );
865 if ( $rigor !== self::RIGOR_QUICK && !$title->isUserConfigPage() ) {
866 # We /could/ use the protection level on the source page, but it's
867 # fairly ugly as we have to establish a precedence hierarchy for pages
868 # included by multiple cascade-protected pages. So just restrict
869 # it to people with 'protect' permission, as they could remove the
870 # protection anyway.
871 list( $cascadingSources, $restrictions ) = $title->getCascadeProtectionSources();
872 # Cascading protection depends on more than this page...
873 # Several cascading protected pages may include this page...
874 # Check each cascading level
875 # This is only for protection restrictions, not for all actions
876 if ( isset( $restrictions[$action] ) ) {
877 foreach ( $restrictions[$action] as $right ) {
878 // Backwards compatibility, rewrite sysop -> editprotected
879 if ( $right == 'sysop' ) {
880 $right = 'editprotected';
881 }
882 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
883 if ( $right == 'autoconfirmed' ) {
884 $right = 'editsemiprotected';
885 }
886 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
887 $wikiPages = '';
888 /** @var Title $wikiPage */
889 foreach ( $cascadingSources as $wikiPage ) {
890 $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
891 }
892 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
893 }
894 }
895 }
896 }
897
898 return $errors;
899 }
900
901 /**
902 * Check action permissions not already checked in checkQuickPermissions
903 *
904 * @param string $action The action to check
905 * @param User $user User to check
906 * @param array $errors List of current errors
907 * @param string $rigor One of PermissionManager::RIGOR_ constants
908 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
909 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
910 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
911 * @param bool $short Short circuit on first error
912 *
913 * @param LinkTarget $page
914 *
915 * @return array List of errors
916 */
917 private function checkActionPermissions(
918 $action,
919 User $user,
920 $errors,
921 $rigor,
922 $short,
923 LinkTarget $page
924 ) {
925 global $wgDeleteRevisionsLimit, $wgLang;
926
927 // TODO: remove & rework upon further use of LinkTarget
928 $title = Title::newFromLinkTarget( $page );
929
930 if ( $action == 'protect' ) {
931 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
932 // If they can't edit, they shouldn't protect.
933 $errors[] = [ 'protect-cantedit' ];
934 }
935 } elseif ( $action == 'create' ) {
936 $title_protection = $title->getTitleProtection();
937 if ( $title_protection ) {
938 if ( $title_protection['permission'] == ''
939 || !$this->userHasRight( $user, $title_protection['permission'] )
940 ) {
941 $errors[] = [
942 'titleprotected',
943 // TODO: get rid of the User dependency
944 User::whoIs( $title_protection['user'] ),
945 $title_protection['reason']
946 ];
947 }
948 }
949 } elseif ( $action == 'move' ) {
950 // Check for immobile pages
951 if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
952 // Specific message for this case
953 $errors[] = [ 'immobile-source-namespace', $title->getNsText() ];
954 } elseif ( !$title->isMovable() ) {
955 // Less specific message for rarer cases
956 $errors[] = [ 'immobile-source-page' ];
957 }
958 } elseif ( $action == 'move-target' ) {
959 if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
960 $errors[] = [ 'immobile-target-namespace', $title->getNsText() ];
961 } elseif ( !$title->isMovable() ) {
962 $errors[] = [ 'immobile-target-page' ];
963 }
964 } elseif ( $action == 'delete' ) {
965 $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $title );
966 if ( !$tempErrors ) {
967 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
968 $user, $tempErrors, $rigor, true, $title );
969 }
970 if ( $tempErrors ) {
971 // If protection keeps them from editing, they shouldn't be able to delete.
972 $errors[] = [ 'deleteprotected' ];
973 }
974 if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit
975 && !$this->userCan( 'bigdelete', $user, $title ) && $title->isBigDeletion()
976 ) {
977 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
978 }
979 } elseif ( $action === 'undelete' ) {
980 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
981 // Undeleting implies editing
982 $errors[] = [ 'undelete-cantedit' ];
983 }
984 if ( !$title->exists()
985 && count( $this->getPermissionErrorsInternal( 'create', $user, $title, $rigor, true ) )
986 ) {
987 // Undeleting where nothing currently exists implies creating
988 $errors[] = [ 'undelete-cantcreate' ];
989 }
990 }
991 return $errors;
992 }
993
994 /**
995 * Check permissions on special pages & namespaces
996 *
997 * @param string $action The action to check
998 * @param User $user User to check
999 * @param array $errors List of current errors
1000 * @param string $rigor One of PermissionManager::RIGOR_ constants
1001 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1002 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1003 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1004 * @param bool $short Short circuit on first error
1005 *
1006 * @param LinkTarget $page
1007 *
1008 * @return array List of errors
1009 */
1010 private function checkSpecialsAndNSPermissions(
1011 $action,
1012 User $user,
1013 $errors,
1014 $rigor,
1015 $short,
1016 LinkTarget $page
1017 ) {
1018 // TODO: remove & rework upon further use of LinkTarget
1019 $title = Title::newFromLinkTarget( $page );
1020
1021 # Only 'createaccount' can be performed on special pages,
1022 # which don't actually exist in the DB.
1023 if ( $title->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) {
1024 $errors[] = [ 'ns-specialprotected' ];
1025 }
1026
1027 # Check $wgNamespaceProtection for restricted namespaces
1028 if ( $title->isNamespaceProtected( $user ) ) {
1029 $ns = $title->getNamespace() == NS_MAIN ?
1030 wfMessage( 'nstab-main' )->text() : $title->getNsText();
1031 $errors[] = $title->getNamespace() == NS_MEDIAWIKI ?
1032 [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
1033 }
1034
1035 return $errors;
1036 }
1037
1038 /**
1039 * Check sitewide CSS/JSON/JS permissions
1040 *
1041 * @param string $action The action to check
1042 * @param User $user User to check
1043 * @param array $errors List of current errors
1044 * @param string $rigor One of PermissionManager::RIGOR_ constants
1045 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1046 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1047 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1048 * @param bool $short Short circuit on first error
1049 *
1050 * @param LinkTarget $page
1051 *
1052 * @return array List of errors
1053 */
1054 private function checkSiteConfigPermissions(
1055 $action,
1056 User $user,
1057 $errors,
1058 $rigor,
1059 $short,
1060 LinkTarget $page
1061 ) {
1062 // TODO: remove & rework upon further use of LinkTarget
1063 $title = Title::newFromLinkTarget( $page );
1064
1065 if ( $action != 'patrol' ) {
1066 $error = null;
1067 // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
1068 // editinterface right. That's implemented as a restriction so no check needed here.
1069 if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) {
1070 $error = [ 'sitecssprotected', $action ];
1071 } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) {
1072 $error = [ 'sitejsonprotected', $action ];
1073 } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) {
1074 $error = [ 'sitejsprotected', $action ];
1075 } elseif ( $title->isRawHtmlMessage() ) {
1076 // Raw HTML can be used to deploy CSS or JS so require rights for both.
1077 if ( !$this->userHasRight( $user, 'editsitejs' ) ) {
1078 $error = [ 'sitejsprotected', $action ];
1079 } elseif ( !$this->userHasRight( $user, 'editsitecss' ) ) {
1080 $error = [ 'sitecssprotected', $action ];
1081 }
1082 }
1083
1084 if ( $error ) {
1085 if ( $this->userHasRight( $user, 'editinterface' ) ) {
1086 // Most users / site admins will probably find out about the new, more restrictive
1087 // permissions by failing to edit something. Give them more info.
1088 // TODO remove this a few release cycles after 1.32
1089 $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
1090 }
1091 $errors[] = $error;
1092 }
1093 }
1094
1095 return $errors;
1096 }
1097
1098 /**
1099 * Check CSS/JSON/JS sub-page permissions
1100 *
1101 * @param string $action The action to check
1102 * @param User $user User to check
1103 * @param array $errors List of current errors
1104 * @param string $rigor One of PermissionManager::RIGOR_ constants
1105 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
1106 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
1107 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
1108 * @param bool $short Short circuit on first error
1109 *
1110 * @param LinkTarget $page
1111 *
1112 * @return array List of errors
1113 */
1114 private function checkUserConfigPermissions(
1115 $action,
1116 User $user,
1117 $errors,
1118 $rigor,
1119 $short,
1120 LinkTarget $page
1121 ) {
1122 // TODO: remove & rework upon further use of LinkTarget
1123 $title = Title::newFromLinkTarget( $page );
1124
1125 # Protect css/json/js subpages of user pages
1126 # XXX: this might be better using restrictions
1127
1128 if ( $action === 'patrol' ) {
1129 return $errors;
1130 }
1131
1132 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) {
1133 // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1134 if (
1135 $title->isUserCssConfigPage()
1136 && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
1137 ) {
1138 $errors[] = [ 'mycustomcssprotected', $action ];
1139 } elseif (
1140 $title->isUserJsonConfigPage()
1141 && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
1142 ) {
1143 $errors[] = [ 'mycustomjsonprotected', $action ];
1144 } elseif (
1145 $title->isUserJsConfigPage()
1146 && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
1147 ) {
1148 $errors[] = [ 'mycustomjsprotected', $action ];
1149 } elseif (
1150 $title->isUserJsConfigPage()
1151 && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' )
1152 ) {
1153 // T207750 - do not allow users to edit a redirect if they couldn't edit the target
1154 $rev = $this->revisionLookup->getRevisionByTitle( $title );
1155 $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null;
1156 $target = $content ? $content->getUltimateRedirectTarget() : null;
1157 if ( $target && (
1158 !$target->inNamespace( NS_USER )
1159 || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
1160 ) ) {
1161 $errors[] = [ 'mycustomjsredirectprotected', $action ];
1162 }
1163 }
1164 } else {
1165 // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
1166 // deletion/suppression which cannot be used for attacks and we want to avoid the
1167 // situation where an unprivileged user can post abusive content on their subpages
1168 // and only very highly privileged users could remove it.
1169 if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
1170 if (
1171 $title->isUserCssConfigPage()
1172 && !$this->userHasRight( $user, 'editusercss' )
1173 ) {
1174 $errors[] = [ 'customcssprotected', $action ];
1175 } elseif (
1176 $title->isUserJsonConfigPage()
1177 && !$this->userHasRight( $user, 'edituserjson' )
1178 ) {
1179 $errors[] = [ 'customjsonprotected', $action ];
1180 } elseif (
1181 $title->isUserJsConfigPage()
1182 && !$this->userHasRight( $user, 'edituserjs' )
1183 ) {
1184 $errors[] = [ 'customjsprotected', $action ];
1185 }
1186 }
1187 }
1188
1189 return $errors;
1190 }
1191
1192 /**
1193 * Testing a permission
1194 *
1195 * @since 1.34
1196 *
1197 * @param UserIdentity $user
1198 * @param string $action
1199 *
1200 * @return bool
1201 */
1202 public function userHasRight( UserIdentity $user, $action = '' ) {
1203 if ( $action === '' ) {
1204 return true; // In the spirit of DWIM
1205 }
1206 // Use strict parameter to avoid matching numeric 0 accidentally inserted
1207 // by misconfiguration: 0 == 'foo'
1208 return in_array( $action, $this->getUserPermissions( $user ), true );
1209 }
1210
1211 /**
1212 * Get the permissions this user has.
1213 *
1214 * @since 1.34
1215 *
1216 * @param UserIdentity $user
1217 *
1218 * @return string[] permission names
1219 */
1220 public function getUserPermissions( UserIdentity $user ) {
1221 $user = User::newFromIdentity( $user );
1222 if ( !isset( $this->usersRights[ $user->getId() ] ) ) {
1223 $this->usersRights[ $user->getId() ] = $this->getGroupPermissions(
1224 $user->getEffectiveGroups()
1225 );
1226 Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] );
1227
1228 // Deny any rights denied by the user's session, unless this
1229 // endpoint has no sessions.
1230 if ( !defined( 'MW_NO_SESSION' ) ) {
1231 // FIXME: $user->getRequest().. need to be replaced with something else
1232 $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
1233 if ( $allowedRights !== null ) {
1234 $this->usersRights[ $user->getId() ] = array_intersect(
1235 $this->usersRights[ $user->getId() ],
1236 $allowedRights
1237 );
1238 }
1239 }
1240
1241 Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] );
1242 // Force reindexation of rights when a hook has unset one of them
1243 $this->usersRights[ $user->getId() ] = array_values(
1244 array_unique( $this->usersRights[ $user->getId() ] )
1245 );
1246
1247 if (
1248 $user->isLoggedIn() &&
1249 $this->blockDisablesLogin &&
1250 $user->getBlock()
1251 ) {
1252 $anon = new User;
1253 $this->usersRights[ $user->getId() ] = array_intersect(
1254 $this->usersRights[ $user->getId() ],
1255 $this->getUserPermissions( $anon )
1256 );
1257 }
1258 }
1259 $rights = $this->usersRights[ $user->getId() ];
1260 foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) {
1261 $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1262 }
1263 return $rights;
1264 }
1265
1266 /**
1267 * Clears users permissions cache, if specific user is provided it tries to clear
1268 * permissions cache only for provided user.
1269 *
1270 * @since 1.34
1271 *
1272 * @param User|null $user
1273 */
1274 public function invalidateUsersRightsCache( $user = null ) {
1275 if ( $user !== null ) {
1276 if ( isset( $this->usersRights[ $user->getId() ] ) ) {
1277 unset( $this->usersRights[$user->getId()] );
1278 }
1279 } else {
1280 $this->usersRights = null;
1281 }
1282 }
1283
1284 /**
1285 * Check, if the given group has the given permission
1286 *
1287 * If you're wanting to check whether all users have a permission, use
1288 * PermissionManager::isEveryoneAllowed() instead. That properly checks if it's revoked
1289 * from anyone.
1290 *
1291 * @since 1.34
1292 *
1293 * @param string $group Group to check
1294 * @param string $role Role to check
1295 *
1296 * @return bool
1297 */
1298 public function groupHasPermission( $group, $role ) {
1299 return isset( $this->groupPermissions[$group][$role] ) &&
1300 $this->groupPermissions[$group][$role] &&
1301 !( isset( $this->revokePermissions[$group][$role] ) &&
1302 $this->revokePermissions[$group][$role] );
1303 }
1304
1305 /**
1306 * Get the permissions associated with a given list of groups
1307 *
1308 * @since 1.34
1309 *
1310 * @param array $groups Array of Strings List of internal group names
1311 * @return array Array of Strings List of permission key names for given groups combined
1312 */
1313 public function getGroupPermissions( $groups ) {
1314 $rights = [];
1315 // grant every granted permission first
1316 foreach ( $groups as $group ) {
1317 if ( isset( $this->groupPermissions[$group] ) ) {
1318 $rights = array_merge( $rights,
1319 // array_filter removes empty items
1320 array_keys( array_filter( $this->groupPermissions[$group] ) ) );
1321 }
1322 }
1323 // now revoke the revoked permissions
1324 foreach ( $groups as $group ) {
1325 if ( isset( $this->revokePermissions[$group] ) ) {
1326 $rights = array_diff( $rights,
1327 array_keys( array_filter( $this->revokePermissions[$group] ) ) );
1328 }
1329 }
1330 return array_unique( $rights );
1331 }
1332
1333 /**
1334 * Get all the groups who have a given permission
1335 *
1336 * @since 1.34
1337 *
1338 * @param string $role Role to check
1339 * @return array Array of Strings List of internal group names with the given permission
1340 */
1341 public function getGroupsWithPermission( $role ) {
1342 $allowedGroups = [];
1343 foreach ( array_keys( $this->groupPermissions ) as $group ) {
1344 if ( $this->groupHasPermission( $group, $role ) ) {
1345 $allowedGroups[] = $group;
1346 }
1347 }
1348 return $allowedGroups;
1349 }
1350
1351 /**
1352 * Check if all users may be assumed to have the given permission
1353 *
1354 * We generally assume so if the right is granted to '*' and isn't revoked
1355 * on any group. It doesn't attempt to take grants or other extension
1356 * limitations on rights into account in the general case, though, as that
1357 * would require it to always return false and defeat the purpose.
1358 * Specifically, session-based rights restrictions (such as OAuth or bot
1359 * passwords) are applied based on the current session.
1360 *
1361 * @param string $right Right to check
1362 *
1363 * @return bool
1364 * @since 1.34
1365 */
1366 public function isEveryoneAllowed( $right ) {
1367 // Use the cached results, except in unit tests which rely on
1368 // being able change the permission mid-request
1369 if ( isset( $this->cachedRights[$right] ) ) {
1370 return $this->cachedRights[$right];
1371 }
1372
1373 if ( !isset( $this->groupPermissions['*'][$right] )
1374 || !$this->groupPermissions['*'][$right] ) {
1375 $this->cachedRights[$right] = false;
1376 return false;
1377 }
1378
1379 // If it's revoked anywhere, then everyone doesn't have it
1380 foreach ( $this->revokePermissions as $rights ) {
1381 if ( isset( $rights[$right] ) && $rights[$right] ) {
1382 $this->cachedRights[$right] = false;
1383 return false;
1384 }
1385 }
1386
1387 // Remove any rights that aren't allowed to the global-session user,
1388 // unless there are no sessions for this endpoint.
1389 if ( !defined( 'MW_NO_SESSION' ) ) {
1390
1391 // XXX: think what could be done with the below
1392 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
1393 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
1394 $this->cachedRights[$right] = false;
1395 return false;
1396 }
1397 }
1398
1399 // Allow extensions to say false
1400 if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
1401 $this->cachedRights[$right] = false;
1402 return false;
1403 }
1404
1405 $this->cachedRights[$right] = true;
1406 return true;
1407 }
1408
1409 /**
1410 * Get a list of all available permissions.
1411 *
1412 * @since 1.34
1413 *
1414 * @return string[] Array of permission names
1415 */
1416 public function getAllPermissions() {
1417 if ( $this->allRights === false ) {
1418 if ( count( $this->availableRights ) ) {
1419 $this->allRights = array_unique( array_merge(
1420 $this->coreRights,
1421 $this->availableRights
1422 ) );
1423 } else {
1424 $this->allRights = $this->coreRights;
1425 }
1426 Hooks::run( 'UserGetAllRights', [ &$this->allRights ] );
1427 }
1428 return $this->allRights;
1429 }
1430
1431 /**
1432 * Add temporary user rights, only valid for the current scope.
1433 * This is meant for making it possible to programatically trigger certain actions that
1434 * the user wouldn't be able to trigger themselves; e.g. allow users without the bot right
1435 * to make bot-flagged actions through certain special pages.
1436 * Returns a "scope guard" variable; whenever that variable goes out of scope or is consumed
1437 * via ScopedCallback::consume(), the temporary rights are revoked.
1438 *
1439 * @since 1.34
1440 *
1441 * @param UserIdentity $user
1442 * @param string|string[] $rights
1443 * @return ScopedCallback
1444 */
1445 public function addTemporaryUserRights( UserIdentity $user, $rights ) {
1446 $userId = $user->getId();
1447 $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
1448 $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
1449 return new ScopedCallback( function () use ( $userId, $nextKey ) {
1450 unset( $this->temporaryUserRights[$userId][$nextKey] );
1451 } );
1452 }
1453
1454 /**
1455 * Overrides user permissions cache
1456 *
1457 * @since 1.34
1458 *
1459 * @param User $user
1460 * @param string[]|string $rights
1461 *
1462 * @throws Exception
1463 */
1464 public function overrideUserRightsForTesting( $user, $rights = [] ) {
1465 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1466 throw new Exception( __METHOD__ . ' can not be called outside of tests' );
1467 }
1468 $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ];
1469 }
1470
1471 }