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