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