MediaWiki  master
PermissionManager.php
Go to the documentation of this file.
1 <?php
20 namespace MediaWiki\Permissions;
21 
22 use Action;
23 use Article;
24 use Exception;
41 use NamespaceInfo;
42 use RequestContext;
43 use SpecialPage;
44 use Title;
45 use TitleFormatter;
46 use User;
47 use UserCache;
48 use Wikimedia\ScopedCallback;
49 
57 
59  public const RIGOR_QUICK = 'quick';
60 
62  public const RIGOR_FULL = 'full';
63 
65  public const RIGOR_SECURE = 'secure';
66 
70  public const CONSTRUCTOR_OPTIONS = [
82  ];
83 
85  private $options;
86 
89 
91  private $redirectLookup;
92 
94  private $nsInfo;
95 
98 
101 
103  private $allRights;
104 
107 
109  private $hookRunner;
110 
112  private $userCache;
113 
116 
119 
122 
124  private $userFactory;
125 
127  private $usersRights = [];
128 
133  private $temporaryUserRights = [];
134 
136  private $cachedRights = [];
137 
144  private $coreRights = [
145  'apihighlimits',
146  'applychangetags',
147  'autoconfirmed',
148  'autocreateaccount',
149  'autopatrol',
150  'bigdelete',
151  'block',
152  'blockemail',
153  'bot',
154  'browsearchive',
155  'changetags',
156  'createaccount',
157  'createpage',
158  'createtalk',
159  'delete',
160  'delete-redirect',
161  'deletechangetags',
162  'deletedhistory',
163  'deletedtext',
164  'deletelogentry',
165  'deleterevision',
166  'edit',
167  'editcontentmodel',
168  'editinterface',
169  'editprotected',
170  'editmyoptions',
171  'editmyprivateinfo',
172  'editmyusercss',
173  'editmyuserjson',
174  'editmyuserjs',
175  'editmyuserjsredirect',
176  'editmywatchlist',
177  'editsemiprotected',
178  'editsitecss',
179  'editsitejson',
180  'editsitejs',
181  'editusercss',
182  'edituserjson',
183  'edituserjs',
184  'hideuser',
185  'import',
186  'importupload',
187  'ipblock-exempt',
188  'managechangetags',
189  'markbotedits',
190  'mergehistory',
191  'minoredit',
192  'move',
193  'movefile',
194  'move-categorypages',
195  'move-rootuserpages',
196  'move-subpages',
197  'nominornewtalk',
198  'noratelimit',
199  'override-export-depth',
200  'pagelang',
201  'patrol',
202  'patrolmarks',
203  'protect',
204  'purge',
205  'read',
206  'reupload',
207  'reupload-own',
208  'reupload-shared',
209  'rollback',
210  'sendemail',
211  'siteadmin',
212  'suppressionlog',
213  'suppressredirect',
214  'suppressrevision',
215  'unblockself',
216  'undelete',
217  'unwatchedpages',
218  'upload',
219  'upload_by_url',
220  'userrights',
221  'userrights-interwiki',
222  'viewmyprivateinfo',
223  'viewmywatchlist',
224  'viewsuppressed',
225  'writeapi',
226  ];
227 
243  public function __construct(
250  HookContainer $hookContainer,
257  ) {
258  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
259  $this->options = $options;
260  $this->specialPageFactory = $specialPageFactory;
261  $this->nsInfo = $nsInfo;
262  $this->groupPermissionsLookup = $groupPermissionsLookup;
263  $this->userGroupManager = $userGroupManager;
264  $this->blockErrorFormatter = $blockErrorFormatter;
265  $this->hookRunner = new HookRunner( $hookContainer );
266  $this->userCache = $userCache;
267  $this->redirectLookup = $redirectLookup;
268  $this->restrictionStore = $restrictionStore;
269  $this->titleFormatter = $titleFormatter;
270  $this->tempUserConfig = $tempUserConfig;
271  $this->userFactory = $userFactory;
272  }
273 
291  public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
292  return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
293  }
294 
310  public function quickUserCan( $action, User $user, LinkTarget $page ) {
311  return $this->userCan( $action, $user, $page, self::RIGOR_QUICK );
312  }
313 
331  public function getPermissionErrors(
332  $action,
333  User $user,
334  LinkTarget $page,
335  $rigor = self::RIGOR_SECURE,
336  $ignoreErrors = []
337  ) {
338  $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
339 
340  // Remove the errors being ignored.
341  foreach ( $errors as $index => $error ) {
342  $errKey = is_array( $error ) ? $error[0] : $error;
343 
344  if ( in_array( $errKey, $ignoreErrors ) ) {
345  unset( $errors[$index] );
346  }
347  if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
348  unset( $errors[$index] );
349  }
350  }
351 
352  return $errors;
353  }
354 
365  public function isBlockedFrom( User $user, $page, $fromReplica = false ) {
366  $block = $user->getBlock( $fromReplica );
367  if ( !$block ) {
368  return false;
369  }
370 
371  if ( $page instanceof PageIdentity ) {
373  } else {
375  }
376 
377  $blocked = $user->isHidden();
378  if ( !$blocked ) {
379  // Special handling for a user's own talk page. The block is not aware
380  // of the user, so this must be done here.
381  if ( $title->equals( $user->getTalkPage() ) ) {
382  $blocked = $block->appliesToUsertalk( $title );
383  } else {
384  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
385  $blocked = $block->appliesToTitle( $title );
386  }
387  }
388 
389  // only for the purpose of the hook. We really don't need this here.
390  $allowUsertalk = $block->isUsertalkEditAllowed();
391 
392  // Allow extensions to let a blocked user access a particular page
393  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
394  $this->hookRunner->onUserIsBlockedFrom( $user, $title, $blocked, $allowUsertalk );
395 
396  return $blocked;
397  }
398 
416  private function getPermissionErrorsInternal(
417  $action,
418  User $user,
419  LinkTarget $page,
420  $rigor = self::RIGOR_SECURE,
421  $short = false
422  ) {
423  if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
424  throw new Exception( "Invalid rigor parameter '$rigor'." );
425  }
426 
427  // With RIGOR_QUICK we can assume automatic account creation will
428  // occur. At a higher rigor level, the caller is required to opt
429  // in by either setting the create intent or actually creating
430  // the account.
431  if ( $rigor === self::RIGOR_QUICK
432  && !$user->isRegistered()
433  && $this->tempUserConfig->isAutoCreateAction( $action )
434  ) {
435  $user = $this->userFactory->newTempPlaceholder();
436  }
437 
438  # Read has special handling
439  if ( $action == 'read' ) {
440  $checks = [
441  'checkPermissionHooks',
442  'checkReadPermissions',
443  'checkUserBlock', // for wgBlockDisablesLogin
444  ];
445  # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
446  # or checkUserConfigPermissions here as it will lead to duplicate
447  # error messages. This is okay to do since anywhere that checks for
448  # create will also check for edit, and those checks are called for edit.
449  } elseif ( $action == 'create' ) {
450  $checks = [
451  'checkQuickPermissions',
452  'checkPermissionHooks',
453  'checkPageRestrictions',
454  'checkCascadingSourcesRestrictions',
455  'checkActionPermissions',
456  'checkUserBlock'
457  ];
458  } else {
459  $checks = [
460  'checkQuickPermissions',
461  'checkPermissionHooks',
462  'checkSpecialsAndNSPermissions',
463  'checkSiteConfigPermissions',
464  'checkUserConfigPermissions',
465  'checkPageRestrictions',
466  'checkCascadingSourcesRestrictions',
467  'checkActionPermissions',
468  'checkUserBlock'
469  ];
470 
471  // Exclude checkUserConfigPermissions on actions that cannot change the
472  // content of the configuration pages.
473  $skipUserConfigActions = [
474  // Allow patrolling per T21818
475  'patrol',
476 
477  // Allow admins and oversighters to delete. For user pages we want to avoid the
478  // situation where an unprivileged user can post abusive content on
479  // their subpages and only very highly privileged users could remove it.
480  // See T200176.
481  'delete',
482  'deleterevision',
483  'suppressrevision',
484 
485  // Allow admins and oversighters to view deleted content, even if they
486  // cannot restore it. See T202989
487  'deletedhistory',
488  'deletedtext',
489  'viewsuppressed',
490  ];
491 
492  if ( in_array( $action, $skipUserConfigActions, true ) ) {
493  $checks = array_diff(
494  $checks,
495  [ 'checkUserConfigPermissions' ]
496  );
497  // Reset numbering
498  $checks = array_values( $checks );
499  }
500  }
501 
502  $errors = [];
503  foreach ( $checks as $method ) {
504  $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
505 
506  if ( $short && $errors !== [] ) {
507  break;
508  }
509  }
510  // remove duplicate errors
511  $errors = array_unique( $errors, SORT_REGULAR );
512 
513  return $errors;
514  }
515 
532  private function checkPermissionHooks(
533  $action,
534  User $user,
535  $errors,
536  $rigor,
537  $short,
538  LinkTarget $page
539  ) {
540  // TODO: remove when LinkTarget usage will expand further
541  $title = Title::newFromLinkTarget( $page );
542  // Use getUserPermissionsErrors instead
543  $result = '';
544  if ( !$this->hookRunner->onUserCan( $title, $user, $action, $result ) ) {
545  return $result ? [] : [ [ 'badaccess-group0' ] ];
546  }
547  // Check getUserPermissionsErrors hook
548  if ( !$this->hookRunner->onGetUserPermissionsErrors( $title, $user, $action, $result ) ) {
549  $errors = $this->resultToError( $errors, $result );
550  }
551  // Check getUserPermissionsErrorsExpensive hook
552  if (
553  $rigor !== self::RIGOR_QUICK
554  && !( $short && count( $errors ) > 0 )
555  && !$this->hookRunner->onGetUserPermissionsErrorsExpensive(
556  $title, $user, $action, $result )
557  ) {
558  $errors = $this->resultToError( $errors, $result );
559  }
560 
561  return $errors;
562  }
563 
572  private function resultToError( $errors, $result ) {
573  if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
574  // A single array representing an error
575  $errors[] = $result;
576  } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
577  // A nested array representing multiple errors
578  $errors = array_merge( $errors, $result );
579  } elseif ( $result !== '' && is_string( $result ) ) {
580  // A string representing a message-id
581  $errors[] = [ $result ];
582  } elseif ( $result instanceof MessageSpecifier ) {
583  // A message specifier representing an error
584  $errors[] = [ $result ];
585  } elseif ( $result === false ) {
586  // a generic "We don't want them to do that"
587  $errors[] = [ 'badaccess-group0' ];
588  }
589  return $errors;
590  }
591 
608  private function checkReadPermissions(
609  $action,
610  User $user,
611  $errors,
612  $rigor,
613  $short,
614  LinkTarget $page
615  ) {
616  // TODO: remove when LinkTarget usage will expand further
617  $title = Title::newFromLinkTarget( $page );
618 
619  $whiteListRead = $this->options->get( MainConfigNames::WhitelistRead );
620  $allowed = false;
621  if ( $this->isEveryoneAllowed( 'read' ) ) {
622  # Shortcut for public wikis, allows skipping quite a bit of code
623  $allowed = true;
624  } elseif ( $this->userHasRight( $user, 'read' ) ) {
625  # If the user is allowed to read pages, he is allowed to read all pages
626  $allowed = true;
627  } elseif ( $this->isSameSpecialPage( 'Userlogin', $title )
628  || $this->isSameSpecialPage( 'PasswordReset', $title )
629  || $this->isSameSpecialPage( 'Userlogout', $title )
630  ) {
631  # Always grant access to the login page.
632  # Even anons need to be able to log in.
633  $allowed = true;
634  } elseif ( $this->isSameSpecialPage( 'RunJobs', $title ) ) {
635  # relies on HMAC key signature alone
636  $allowed = true;
637  } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
638  # Time to check the whitelist
639  # Only do these checks is there's something to check against
640  $name = $title->getPrefixedText();
641  $dbName = $title->getPrefixedDBkey();
642 
643  // Check for explicit whitelisting with and without underscores
644  if ( in_array( $name, $whiteListRead, true )
645  || in_array( $dbName, $whiteListRead, true )
646  ) {
647  $allowed = true;
648  } elseif ( $title->getNamespace() === NS_MAIN ) {
649  # Old settings might have the title prefixed with
650  # a colon for main-namespace pages
651  if ( in_array( ':' . $name, $whiteListRead ) ) {
652  $allowed = true;
653  }
654  } elseif ( $title->isSpecialPage() ) {
655  # If it's a special page, ditch the subpage bit and check again
656  $name = $title->getDBkey();
657  list( $name, /* $subpage */ ) =
658  $this->specialPageFactory->resolveAlias( $name );
659  if ( $name ) {
660  $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
661  if ( in_array( $pure, $whiteListRead, true ) ) {
662  $allowed = true;
663  }
664  }
665  }
666  }
667 
668  $whitelistReadRegexp = $this->options->get( MainConfigNames::WhitelistReadRegexp );
669  if ( !$allowed && is_array( $whitelistReadRegexp )
670  && !empty( $whitelistReadRegexp )
671  ) {
672  $name = $title->getPrefixedText();
673  // Check for regex whitelisting
674  foreach ( $whitelistReadRegexp as $listItem ) {
675  if ( preg_match( $listItem, $name ) ) {
676  $allowed = true;
677  break;
678  }
679  }
680  }
681 
682  if ( !$allowed ) {
683  # If the title is not whitelisted, give extensions a chance to do so...
684  $this->hookRunner->onTitleReadWhitelist( $title, $user, $allowed );
685  if ( !$allowed ) {
686  $errors[] = $this->missingPermissionError( $action, $short );
687  }
688  }
689 
690  return $errors;
691  }
692 
701  private function missingPermissionError( $action, $short ) {
702  // We avoid expensive display logic for quickUserCan's and such
703  if ( $short ) {
704  return [ 'badaccess-group0' ];
705  }
706 
707  // TODO: it would be a good idea to replace the method below with something else like
708  // maybe callback injection
709  return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
710  }
711 
720  private function isSameSpecialPage( $name, LinkTarget $page ) {
721  if ( $page->getNamespace() === NS_SPECIAL ) {
722  list( $thisName, /* $subpage */ ) =
723  $this->specialPageFactory->resolveAlias( $page->getDBkey() );
724  if ( $name == $thisName ) {
725  return true;
726  }
727  }
728  return false;
729  }
730 
747  private function checkUserBlock(
748  $action,
749  User $user,
750  $errors,
751  $rigor,
752  $short,
753  LinkTarget $page
754  ) {
755  // Unblocking handled in SpecialUnblock
756  if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'unblock' ] ) ) {
757  return $errors;
758  }
759 
760  // Optimize for a very common case
761  if ( $action === 'read' && !$this->options->get( MainConfigNames::BlockDisablesLogin ) ) {
762  return $errors;
763  }
764 
765  if ( $this->options->get( MainConfigNames::EmailConfirmToEdit )
766  && !$user->isEmailConfirmed()
767  && $action === 'edit'
768  ) {
769  $errors[] = [ 'confirmedittext' ];
770  }
771 
772  switch ( $rigor ) {
773  case self::RIGOR_SECURE:
774  $blockInfoFreshness = Authority::READ_LATEST;
775  $useReplica = false;
776  break;
777  case self::RIGOR_FULL:
778  $blockInfoFreshness = Authority::READ_NORMAL;
779  $useReplica = true;
780  break;
781  default:
782  $useReplica = true;
783  $blockInfoFreshness = Authority::READ_NORMAL;
784  }
785 
786  $block = $user->getBlock( $blockInfoFreshness );
787 
788  if ( $action === 'createaccount' ) {
789  $applicableBlock = null;
790  if ( $block && $block->appliesToRight( 'createaccount' ) ) {
791  $applicableBlock = $block;
792  }
793 
794  # T15611: if the IP address the user is trying to create an account from is
795  # blocked with createaccount disabled, prevent new account creation there even
796  # when the user is logged in
797  if ( !$this->userHasRight( $user, 'ipblock-exempt' ) ) {
798  $ipBlock = DatabaseBlock::newFromTarget(
799  null, $user->getRequest()->getIP()
800  );
801  if ( $ipBlock && $ipBlock->appliesToRight( 'createaccount' ) ) {
802  $applicableBlock = $ipBlock;
803  }
804  }
805  // @todo FIXME: Pass the relevant context into this function.
806  if ( $applicableBlock ) {
808  $message = $this->blockErrorFormatter->getMessage(
809  $applicableBlock,
810  $context->getUser(),
812  $context->getRequest()->getIP()
813  );
814  $errors[] = array_merge( [ $message->getKey() ], $message->getParams() );
815  return $errors;
816  }
817  }
818 
819  // If the user does not have a block, or the block they do have explicitly
820  // allows the action (like "read" or "upload").
821  if ( !$block || $block->appliesToRight( $action ) === false ) {
822  return $errors;
823  }
824 
825  // Determine if the user is blocked from this action on this page.
826  // What gets passed into this method is a user right, not an action name.
827  // There is no way to instantiate an action by restriction. However, this
828  // will get the action where the restriction is the same. This may result
829  // in actions being blocked that shouldn't be.
830  $actionObj = null;
831  $title = Title::newFromLinkTarget( $page, 'clone' );
832  if ( $title->canExist() ) {
833  // TODO: this drags a ton of dependencies in, would be good to avoid Article
834  // instantiation and decouple it creating an ActionPermissionChecker interface
835  // Creating an action will perform several database queries to ensure that
836  // the action has not been overridden by the content type.
837  // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
838  // probably we may use fake context object since it's unlikely that Action uses it
839  // anyway. It would be nice if we could avoid instantiating the Action at all.
841  $actionObj = Action::factory(
842  $action,
844  $context
845  );
846  // Ensure that the retrieved action matches the restriction.
847  if ( $actionObj && $actionObj->getRestriction() !== $action ) {
848  $actionObj = null;
849  }
850  }
851 
852  // If no action object is returned, assume that the action requires unblock
853  // which is the default.
854  if ( !$actionObj || $actionObj->requiresUnblock() ) {
855  if (
856  $this->isBlockedFrom( $user, $page, $useReplica ) ||
857  (
858  $this->options->get( MainConfigNames::EnablePartialActionBlocks ) &&
859  $block->appliesToRight( $action )
860  )
861  ) {
862  // @todo FIXME: Pass the relevant context into this function.
864  $message = $this->blockErrorFormatter->getMessage(
865  $block,
866  $context->getUser(),
868  $context->getRequest()->getIP()
869  );
870  $errors[] = array_merge( [ $message->getKey() ], $message->getParams() );
871  }
872  }
873 
874  return $errors;
875  }
876 
893  private function checkQuickPermissions(
894  $action,
895  User $user,
896  $errors,
897  $rigor,
898  $short,
899  LinkTarget $page
900  ) {
901  // TODO: remove when LinkTarget usage will expand further
902  $title = Title::newFromLinkTarget( $page );
903 
904  if ( !$this->hookRunner->onTitleQuickPermissions( $title, $user, $action,
905  $errors, $rigor !== self::RIGOR_QUICK, $short )
906  ) {
907  return $errors;
908  }
909 
910  $isSubPage = $this->nsInfo->hasSubpages( $title->getNamespace() ) ?
911  strpos( $title->getText(), '/' ) !== false : false;
912 
913  if ( $action == 'create' ) {
914  if (
915  ( $this->nsInfo->isTalk( $title->getNamespace() ) &&
916  !$this->userHasRight( $user, 'createtalk' ) ) ||
917  ( !$this->nsInfo->isTalk( $title->getNamespace() ) &&
918  !$this->userHasRight( $user, 'createpage' ) )
919  ) {
920  $errors[] = $user->isNamed() ? [ 'nocreate-loggedin' ] : [ 'nocreatetext' ];
921  }
922  } elseif ( $action == 'move' ) {
923  if ( !$this->userHasRight( $user, 'move-rootuserpages' )
924  && $title->getNamespace() === NS_USER && !$isSubPage
925  ) {
926  // Show user page-specific message only if the user can move other pages
927  $errors[] = [ 'cant-move-user-page' ];
928  }
929 
930  // Check if user is allowed to move files if it's a file
931  if ( $title->getNamespace() === NS_FILE &&
932  !$this->userHasRight( $user, 'movefile' )
933  ) {
934  $errors[] = [ 'movenotallowedfile' ];
935  }
936 
937  // Check if user is allowed to move category pages if it's a category page
938  if ( $title->getNamespace() === NS_CATEGORY &&
939  !$this->userHasRight( $user, 'move-categorypages' )
940  ) {
941  $errors[] = [ 'cant-move-category-page' ];
942  }
943 
944  if ( !$this->userHasRight( $user, 'move' ) ) {
945  // User can't move anything
946  $userCanMove = $this->groupPermissionsLookup
947  ->groupHasPermission( 'user', 'move' );
948  $namedCanMove = $this->groupPermissionsLookup
949  ->groupHasPermission( 'named', 'move' );
950  $autoconfirmedCanMove = $this->groupPermissionsLookup
951  ->groupHasPermission( 'autoconfirmed', 'move' );
952  if ( $user->isAnon()
953  && ( $userCanMove || $namedCanMove || $autoconfirmedCanMove )
954  ) {
955  // custom message if logged-in users without any special rights can move
956  $errors[] = [ 'movenologintext' ];
957  } elseif ( $user->isTemp() && ( $namedCanMove || $autoconfirmedCanMove ) ) {
958  // Temp user may be able to move if they log in as a proper account
959  $errors[] = [ 'movenologintext' ];
960  } else {
961  $errors[] = [ 'movenotallowed' ];
962  }
963  }
964  } elseif ( $action == 'move-target' ) {
965  if ( !$this->userHasRight( $user, 'move' ) ) {
966  // User can't move anything
967  $errors[] = [ 'movenotallowed' ];
968  } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' )
969  && $title->getNamespace() === NS_USER
970  && !$isSubPage
971  ) {
972  // Show user page-specific message only if the user can move other pages
973  $errors[] = [ 'cant-move-to-user-page' ];
974  } elseif ( !$this->userHasRight( $user, 'move-categorypages' )
975  && $title->getNamespace() === NS_CATEGORY
976  ) {
977  // Show category page-specific message only if the user can move other pages
978  $errors[] = [ 'cant-move-to-category-page' ];
979  }
980  } elseif ( !$this->userHasRight( $user, $action ) ) {
981  $errors[] = $this->missingPermissionError( $action, $short );
982  }
983 
984  return $errors;
985  }
986 
1005  private function checkPageRestrictions(
1006  $action,
1007  User $user,
1008  $errors,
1009  $rigor,
1010  $short,
1011  LinkTarget $page
1012  ) {
1013  // TODO: remove & rework upon further use of LinkTarget
1014  $title = Title::newFromLinkTarget( $page );
1015  foreach ( $this->restrictionStore->getRestrictions( $title, $action ) as $right ) {
1016  // Backwards compatibility, rewrite sysop -> editprotected
1017  if ( $right == 'sysop' ) {
1018  $right = 'editprotected';
1019  }
1020  // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
1021  if ( $right == 'autoconfirmed' ) {
1022  $right = 'editsemiprotected';
1023  }
1024  if ( $right == '' ) {
1025  continue;
1026  }
1027  if ( !$this->userHasRight( $user, $right ) ) {
1028  $errors[] = [ 'protectedpagetext', $right, $action ];
1029  } elseif ( $this->restrictionStore->areRestrictionsCascading( $title ) &&
1030  !$this->userHasRight( $user, 'protect' )
1031  ) {
1032  $errors[] = [ 'protectedpagetext', 'protect', $action ];
1033  }
1034  }
1035 
1036  return $errors;
1037  }
1038 
1056  $action,
1057  UserIdentity $user,
1058  $errors,
1059  $rigor,
1060  $short,
1061  LinkTarget $page
1062  ) {
1063  // TODO: remove & rework upon further use of LinkTarget
1064  $title = Title::newFromLinkTarget( $page );
1065  if ( $rigor !== self::RIGOR_QUICK && !$title->isUserConfigPage() ) {
1066  list( $cascadingSources, $restrictions ) = $this->restrictionStore->getCascadeProtectionSources( $title );
1067  # Cascading protection depends on more than this page...
1068  # Several cascading protected pages may include this page...
1069  # Check each cascading level
1070  # This is only for protection restrictions, not for all actions
1071  if ( isset( $restrictions[$action] ) ) {
1072  foreach ( $restrictions[$action] as $right ) {
1073  // Backwards compatibility, rewrite sysop -> editprotected
1074  if ( $right == 'sysop' ) {
1075  $right = 'editprotected';
1076  }
1077  // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
1078  if ( $right == 'autoconfirmed' ) {
1079  $right = 'editsemiprotected';
1080  }
1081  if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) {
1082  $wikiPages = '';
1083  foreach ( $cascadingSources as $pageIdentity ) {
1084  $wikiPages .= '* [[:' . $this->titleFormatter->getPrefixedText( $pageIdentity ) . "]]\n";
1085  }
1086  $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
1087  }
1088  }
1089  }
1090  }
1091 
1092  return $errors;
1093  }
1094 
1111  private function checkActionPermissions(
1112  $action,
1113  User $user,
1114  $errors,
1115  $rigor,
1116  $short,
1117  LinkTarget $page
1118  ) {
1119  global $wgLang;
1120 
1121  // TODO: remove & rework upon further use of LinkTarget
1122  $title = Title::newFromLinkTarget( $page );
1123 
1124  if ( $action == 'protect' ) {
1125  if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
1126  // If they can't edit, they shouldn't protect.
1127  $errors[] = [ 'protect-cantedit' ];
1128  }
1129  } elseif ( $action == 'create' ) {
1130  $title_protection = $title->getTitleProtection();
1131  if ( $title_protection ) {
1132  if ( $title_protection['permission'] == ''
1133  || !$this->userHasRight( $user, $title_protection['permission'] )
1134  ) {
1135  $errors[] = [
1136  'titleprotected',
1137  $this->userCache->getProp( $title_protection['user'], 'name' ),
1138  $title_protection['reason']
1139  ];
1140  }
1141  }
1142  } elseif ( $action == 'move' ) {
1143  // Check for immobile pages
1144  if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
1145  // Specific message for this case
1146  $nsText = $title->getNsText();
1147  if ( $nsText === '' ) {
1148  $nsText = wfMessage( 'blanknamespace' )->text();
1149  }
1150  $errors[] = [ 'immobile-source-namespace', $nsText ];
1151  } elseif ( !$title->isMovable() ) {
1152  // Less specific message for rarer cases
1153  $errors[] = [ 'immobile-source-page' ];
1154  }
1155  } elseif ( $action == 'move-target' ) {
1156  if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
1157  $nsText = $title->getNsText();
1158  if ( $nsText === '' ) {
1159  $nsText = wfMessage( 'blanknamespace' )->text();
1160  }
1161  $errors[] = [ 'immobile-target-namespace', $nsText ];
1162  } elseif ( !$title->isMovable() ) {
1163  $errors[] = [ 'immobile-target-page' ];
1164  }
1165  } elseif ( $action == 'delete' || $action == 'delete-redirect' ) {
1166  $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $title );
1167  if ( !$tempErrors ) {
1168  $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
1169  $user, $tempErrors, $rigor, true, $title );
1170  }
1171  if ( $tempErrors ) {
1172  // If protection keeps them from editing, they shouldn't be able to delete.
1173  $errors[] = [ 'deleteprotected' ];
1174  }
1175  if ( $rigor !== self::RIGOR_QUICK
1176  && $action == 'delete'
1177  && $this->options->get( MainConfigNames::DeleteRevisionsLimit )
1178  && !$this->userCan( 'bigdelete', $user, $title )
1179  && $title->isBigDeletion()
1180  ) {
1181  // NOTE: This check is deprecated since 1.37, see T288759
1182  $errors[] = [
1183  'delete-toobig',
1184  $wgLang->formatNum( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) )
1185  ];
1186  }
1187  } elseif ( $action === 'undelete' ) {
1188  if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
1189  // Undeleting implies editing
1190  $errors[] = [ 'undelete-cantedit' ];
1191  }
1192  if ( !$title->exists()
1193  && count( $this->getPermissionErrorsInternal( 'create', $user, $title, $rigor, true ) )
1194  ) {
1195  // Undeleting where nothing currently exists implies creating
1196  $errors[] = [ 'undelete-cantcreate' ];
1197  }
1198  } elseif ( $action === 'edit' ) {
1199  if ( !$title->exists() ) {
1200  $errors = array_merge(
1201  $errors,
1203  'create',
1204  $user,
1205  $title,
1206  $rigor,
1207  true
1208  )
1209  );
1210  }
1211  }
1212  return $errors;
1213  }
1214 
1232  $action,
1233  UserIdentity $user,
1234  $errors,
1235  $rigor,
1236  $short,
1237  LinkTarget $page
1238  ) {
1239  // TODO: remove & rework upon further use of LinkTarget
1240  $title = Title::newFromLinkTarget( $page );
1241 
1242  # Only 'createaccount' can be performed on special pages,
1243  # which don't actually exist in the DB.
1244  if ( $title->getNamespace() === NS_SPECIAL && $action !== 'createaccount' ) {
1245  $errors[] = [ 'ns-specialprotected' ];
1246  }
1247 
1248  # Check $wgNamespaceProtection for restricted namespaces
1249  if ( $this->isNamespaceProtected( $title->getNamespace(), $user ) ) {
1250  $ns = $title->getNamespace() === NS_MAIN ?
1251  wfMessage( 'nstab-main' )->text() : $title->getNsText();
1252  $errors[] = $title->getNamespace() === NS_MEDIAWIKI ?
1253  [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
1254  }
1255 
1256  return $errors;
1257  }
1258 
1275  private function checkSiteConfigPermissions(
1276  $action,
1277  User $user,
1278  $errors,
1279  $rigor,
1280  $short,
1281  LinkTarget $page
1282  ) {
1283  // TODO: remove & rework upon further use of LinkTarget
1284  $title = Title::newFromLinkTarget( $page );
1285 
1286  if ( $action === 'patrol' ) {
1287  return $errors;
1288  }
1289 
1290  if ( in_array( $action, [ 'deletedhistory', 'deletedtext', 'viewsuppressed' ], true ) ) {
1291  // Allow admins and oversighters to view deleted content, even if they
1292  // cannot restore it. See T202989
1293  // Not using the same handling in `getPermissionErrorsInternal` as the checks
1294  // for skipping `checkUserConfigPermissions` since normal admins can delete
1295  // user scripts, but not sitedwide scripts
1296  return $errors;
1297  }
1298 
1299  // Sitewide CSS/JSON/JS/RawHTML changes, like all NS_MEDIAWIKI changes, also require the
1300  // editinterface right. That's implemented as a restriction so no check needed here.
1301  if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) {
1302  $errors[] = [ 'sitecssprotected', $action ];
1303  } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) {
1304  $errors[] = [ 'sitejsonprotected', $action ];
1305  } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) {
1306  $errors[] = [ 'sitejsprotected', $action ];
1307  }
1308  if ( $title->isRawHtmlMessage() && !$this->userCanEditRawHtmlPage( $user ) ) {
1309  $errors[] = [ 'siterawhtmlprotected', $action ];
1310  }
1311 
1312  return $errors;
1313  }
1314 
1331  private function checkUserConfigPermissions(
1332  $action,
1333  UserIdentity $user,
1334  $errors,
1335  $rigor,
1336  $short,
1337  LinkTarget $page
1338  ) {
1339  // TODO: remove & rework upon further use of LinkTarget
1340  $title = Title::newFromLinkTarget( $page );
1341 
1342  # Protect css/json/js subpages of user pages
1343  # XXX: this might be better using restrictions
1344  if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) {
1345  // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1346  if (
1347  $title->isUserCssConfigPage()
1348  && !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' )
1349  ) {
1350  $errors[] = [ 'mycustomcssprotected', $action ];
1351  } elseif (
1352  $title->isUserJsonConfigPage()
1353  && !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' )
1354  ) {
1355  $errors[] = [ 'mycustomjsonprotected', $action ];
1356  } elseif (
1357  $title->isUserJsConfigPage()
1358  && !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' )
1359  ) {
1360  $errors[] = [ 'mycustomjsprotected', $action ];
1361  } elseif (
1362  $title->isUserJsConfigPage()
1363  && !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' )
1364  ) {
1365  // T207750 - do not allow users to edit a redirect if they couldn't edit the target
1366  $target = $this->redirectLookup->getRedirectTarget( $title );
1367  if ( $target && (
1368  !$target->inNamespace( NS_USER )
1369  || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
1370  ) ) {
1371  $errors[] = [ 'mycustomjsredirectprotected', $action ];
1372  }
1373  }
1374  } else {
1375  // Users need edituser* to edit others' CSS/JSON/JS subpages.
1376  // The checks to exclude deletion/suppression, which cannot be used for
1377  // attacks and should be excluded to avoid the situation where an
1378  // unprivileged user can post abusive content on their subpages
1379  // and only very highly privileged users could remove it,
1380  // are now a part of `getPermissionErrorsInternal` and this method isn't called.
1381  if (
1382  $title->isUserCssConfigPage()
1383  && !$this->userHasRight( $user, 'editusercss' )
1384  ) {
1385  $errors[] = [ 'customcssprotected', $action ];
1386  } elseif (
1387  $title->isUserJsonConfigPage()
1388  && !$this->userHasRight( $user, 'edituserjson' )
1389  ) {
1390  $errors[] = [ 'customjsonprotected', $action ];
1391  } elseif (
1392  $title->isUserJsConfigPage()
1393  && !$this->userHasRight( $user, 'edituserjs' )
1394  ) {
1395  $errors[] = [ 'customjsprotected', $action ];
1396  }
1397  }
1398 
1399  return $errors;
1400  }
1401 
1412  public function userHasRight( UserIdentity $user, $action = '' ) {
1413  if ( $action === '' ) {
1414  return true; // In the spirit of DWIM
1415  }
1416  // Use strict parameter to avoid matching numeric 0 accidentally inserted
1417  // by misconfiguration: 0 == 'foo'
1418  return in_array( $action, $this->getUserPermissions( $user ), true );
1419  }
1420 
1429  public function userHasAnyRight( UserIdentity $user, ...$actions ) {
1430  foreach ( $actions as $action ) {
1431  if ( $this->userHasRight( $user, $action ) ) {
1432  return true;
1433  }
1434  }
1435  return false;
1436  }
1437 
1446  public function userHasAllRights( UserIdentity $user, ...$actions ) {
1447  foreach ( $actions as $action ) {
1448  if ( !$this->userHasRight( $user, $action ) ) {
1449  return false;
1450  }
1451  }
1452  return true;
1453  }
1454 
1464  public function getUserPermissions( UserIdentity $user ) {
1465  $rightsCacheKey = $this->getRightsCacheKey( $user );
1466  if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1467  $userObj = User::newFromIdentity( $user );
1468  $this->usersRights[ $rightsCacheKey ] = $this->getGroupPermissions(
1469  $this->userGroupManager->getUserEffectiveGroups( $user )
1470  );
1471  // Hook requires a full User object
1472  $this->hookRunner->onUserGetRights( $userObj, $this->usersRights[ $rightsCacheKey ] );
1473 
1474  // Deny any rights denied by the user's session, unless this
1475  // endpoint has no sessions.
1476  if ( !defined( 'MW_NO_SESSION' ) ) {
1477  // FIXME: $userObj->getRequest().. need to be replaced with something else
1478  $allowedRights = $userObj->getRequest()->getSession()->getAllowedUserRights();
1479  if ( $allowedRights !== null ) {
1480  $this->usersRights[ $rightsCacheKey ] = array_intersect(
1481  $this->usersRights[ $rightsCacheKey ],
1482  $allowedRights
1483  );
1484  }
1485  }
1486 
1487  // Hook requires a full User object
1488  $this->hookRunner->onUserGetRightsRemove(
1489  $userObj, $this->usersRights[ $rightsCacheKey ] );
1490  // Force reindexation of rights when a hook has unset one of them
1491  $this->usersRights[ $rightsCacheKey ] = array_values(
1492  array_unique( $this->usersRights[ $rightsCacheKey ] )
1493  );
1494 
1495  if (
1496  $userObj->isRegistered() &&
1497  $this->options->get( MainConfigNames::BlockDisablesLogin ) &&
1498  $userObj->getBlock()
1499  ) {
1500  $anon = new User;
1501  $this->usersRights[ $rightsCacheKey ] = array_intersect(
1502  $this->usersRights[ $rightsCacheKey ],
1503  $this->getUserPermissions( $anon )
1504  );
1505  }
1506  }
1507  $rights = $this->usersRights[ $rightsCacheKey ];
1508  foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) {
1509  $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1510  }
1511  return $rights;
1512  }
1513 
1522  public function invalidateUsersRightsCache( $user = null ) {
1523  if ( $user !== null ) {
1524  $rightsCacheKey = $this->getRightsCacheKey( $user );
1525  unset( $this->usersRights[ $rightsCacheKey ] );
1526  } else {
1527  $this->usersRights = [];
1528  }
1529  }
1530 
1536  private function getRightsCacheKey( UserIdentity $user ) {
1537  return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
1538  }
1539 
1555  public function groupHasPermission( $group, $role ) {
1556  return $this->groupPermissionsLookup->groupHasPermission( $group, $role );
1557  }
1558 
1568  public function getGroupPermissions( $groups ) {
1569  return $this->groupPermissionsLookup->getGroupPermissions( $groups );
1570  }
1571 
1581  public function getGroupsWithPermission( $role ) {
1582  return $this->groupPermissionsLookup->getGroupsWithPermission( $role );
1583  }
1584 
1600  public function isEveryoneAllowed( $right ) {
1601  // Use the cached results, except in unit tests which rely on
1602  // being able change the permission mid-request
1603  if ( isset( $this->cachedRights[$right] ) ) {
1604  return $this->cachedRights[$right];
1605  }
1606 
1607  if ( !isset( $this->options->get( MainConfigNames::GroupPermissions )['*'][$right] )
1608  || !$this->options->get( MainConfigNames::GroupPermissions )['*'][$right]
1609  ) {
1610  $this->cachedRights[$right] = false;
1611  return false;
1612  }
1613 
1614  // If it's revoked anywhere, then everyone doesn't have it
1615  foreach ( $this->options->get( MainConfigNames::RevokePermissions ) as $rights ) {
1616  if ( isset( $rights[$right] ) && $rights[$right] ) {
1617  $this->cachedRights[$right] = false;
1618  return false;
1619  }
1620  }
1621 
1622  // Remove any rights that aren't allowed to the global-session user,
1623  // unless there are no sessions for this endpoint.
1624  if ( !defined( 'MW_NO_SESSION' ) ) {
1625 
1626  // XXX: think what could be done with the below
1627  $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
1628  if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
1629  $this->cachedRights[$right] = false;
1630  return false;
1631  }
1632  }
1633 
1634  // Allow extensions to say false
1635  if ( !$this->hookRunner->onUserIsEveryoneAllowed( $right ) ) {
1636  $this->cachedRights[$right] = false;
1637  return false;
1638  }
1639 
1640  $this->cachedRights[$right] = true;
1641  return true;
1642  }
1643 
1651  public function getAllPermissions() {
1652  if ( $this->allRights === null ) {
1653  if ( count( $this->options->get( MainConfigNames::AvailableRights ) ) ) {
1654  $this->allRights = array_unique( array_merge(
1655  $this->coreRights,
1656  $this->options->get( MainConfigNames::AvailableRights )
1657  ) );
1658  } else {
1659  $this->allRights = $this->coreRights;
1660  }
1661  $this->hookRunner->onUserGetAllRights( $this->allRights );
1662  }
1663  return $this->allRights;
1664  }
1665 
1672  private function isNamespaceProtected( $index, UserIdentity $user ) {
1673  $namespaceProtection = $this->options->get( MainConfigNames::NamespaceProtection );
1674  if ( isset( $namespaceProtection[$index] ) ) {
1675  return !$this->userHasAllRights( $user, ...(array)$namespaceProtection[$index] );
1676  }
1677  return false;
1678  }
1679 
1688  public function getNamespaceRestrictionLevels( $index, UserIdentity $user = null ) {
1689  if ( !isset( $this->options->get( MainConfigNames::NamespaceProtection )[$index] ) ) {
1690  // All levels are valid if there's no namespace restriction.
1691  // But still filter by user, if necessary
1692  $levels = $this->options->get( MainConfigNames::RestrictionLevels );
1693  if ( $user ) {
1694  $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
1695  $right = $level;
1696  if ( $right == 'sysop' ) {
1697  $right = 'editprotected'; // BC
1698  }
1699  if ( $right == 'autoconfirmed' ) {
1700  $right = 'editsemiprotected'; // BC
1701  }
1702  return $this->userHasRight( $user, $right );
1703  } ) );
1704  }
1705  return $levels;
1706  }
1707 
1708  // $wgNamespaceProtection can require one or more rights to edit the namespace, which
1709  // may be satisfied by membership in multiple groups each giving a subset of those rights.
1710  // A restriction level is redundant if, for any one of the namespace rights, all groups
1711  // giving that right also give the restriction level's right. Or, conversely, a
1712  // restriction level is not redundant if, for every namespace right, there's at least one
1713  // group giving that right without the restriction level's right.
1714  //
1715  // First, for each right, get a list of groups with that right.
1716  $namespaceRightGroups = [];
1717  foreach ( (array)$this->options->get( MainConfigNames::NamespaceProtection )[$index] as $right ) {
1718  if ( $right == 'sysop' ) {
1719  $right = 'editprotected'; // BC
1720  }
1721  if ( $right == 'autoconfirmed' ) {
1722  $right = 'editsemiprotected'; // BC
1723  }
1724  if ( $right != '' ) {
1725  $namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right );
1726  }
1727  }
1728 
1729  // Now, go through the protection levels one by one.
1730  $usableLevels = [ '' ];
1731  foreach ( $this->options->get( MainConfigNames::RestrictionLevels ) as $level ) {
1732  $right = $level;
1733  if ( $right == 'sysop' ) {
1734  $right = 'editprotected'; // BC
1735  }
1736  if ( $right == 'autoconfirmed' ) {
1737  $right = 'editsemiprotected'; // BC
1738  }
1739 
1740  if ( $right != '' &&
1741  !isset( $namespaceRightGroups[$right] ) &&
1742  ( !$user || $this->userHasRight( $user, $right ) )
1743  ) {
1744  // Do any of the namespace rights imply the restriction right? (see explanation above)
1745  foreach ( $namespaceRightGroups as $groups ) {
1746  if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) {
1747  // Yes, this one does.
1748  continue 2;
1749  }
1750  }
1751  // No, keep the restriction level
1752  $usableLevels[] = $level;
1753  }
1754  }
1755 
1756  return $usableLevels;
1757  }
1758 
1767  private function userCanEditRawHtmlPage( UserIdentity $user ) {
1768  return $this->userHasAllRights( $user, 'editsitecss', 'editsitejs' );
1769  }
1770 
1785  public function addTemporaryUserRights( UserIdentity $user, $rights ) {
1786  $userId = $user->getId();
1787  $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
1788  $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
1789  return new ScopedCallback( function () use ( $userId, $nextKey ) {
1790  unset( $this->temporaryUserRights[$userId][$nextKey] );
1791  } );
1792  }
1793 
1804  public function overrideUserRightsForTesting( $user, $rights = [] ) {
1805  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1806  throw new Exception( __METHOD__ . ' can not be called outside of tests' );
1807  }
1808  $this->usersRights[ $this->getRightsCacheKey( $user ) ] =
1809  is_array( $rights ) ? $rights : [ $rights ];
1810  }
1811 
1812 }
const NS_USER
Definition: Defines.php:66
const NS_FILE
Definition: Defines.php:70
const NS_MAIN
Definition: Defines.php:64
const NS_MEDIAWIKI
Definition: Defines.php:72
const NS_SPECIAL
Definition: Defines.php:53
const NS_CATEGORY
Definition: Defines.php:78
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition: Setup.php:470
Actions are things which can be done to pages (edit, delete, rollback, etc).
Definition: Action.php:43
static factory(string $action, Article $article, IContextSource $context=null)
Get an appropriate Action subclass for the given action.
Definition: Action.php:85
Legacy class representing an editable page and handling UI for some page actions.
Definition: Article.php:47
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:161
A service class for getting formatted information about a block.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
static newFromTarget( $specificTarget, $vagueTarget=null, $fromPrimary=false)
Given a target and the target's type, get an existing block object if possible.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
A class containing constants representing the names of configuration variables.
const AvailableRights
Name constant for the AvailableRights setting, for use with Config::get()
const NamespaceProtection
Name constant for the NamespaceProtection setting, for use with Config::get()
const RevokePermissions
Name constant for the RevokePermissions setting, for use with Config::get()
const WhitelistRead
Name constant for the WhitelistRead setting, for use with Config::get()
const BlockDisablesLogin
Name constant for the BlockDisablesLogin setting, for use with Config::get()
const DeleteRevisionsLimit
Name constant for the DeleteRevisionsLimit setting, for use with Config::get()
const EmailConfirmToEdit
Name constant for the EmailConfirmToEdit setting, for use with Config::get()
const GroupPermissions
Name constant for the GroupPermissions setting, for use with Config::get()
const RestrictionLevels
Name constant for the RestrictionLevels setting, for use with Config::get()
const WhitelistReadRegexp
Name constant for the WhitelistReadRegexp setting, for use with Config::get()
const EnablePartialActionBlocks
Name constant for the EnablePartialActionBlocks setting, for use with Config::get()
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
getRightsCacheKey(UserIdentity $user)
Gets a unique key for user rights cache.
string[][] $usersRights
Cached user rights.
bool[] $cachedRights
Cached rights for isEveryoneAllowed, [ right => allowed ].
checkPermissionHooks( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Check various permission hooks.
missingPermissionError( $action, $short)
Get a description array when the user doesn't have the right to perform $action (i....
checkUserConfigPermissions( $action, UserIdentity $user, $errors, $rigor, $short, LinkTarget $page)
Check CSS/JSON/JS sub-page permissions.
quickUserCan( $action, User $user, LinkTarget $page)
A convenience method for calling PermissionManager::userCan with PermissionManager::RIGOR_QUICK.
checkReadPermissions( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Check that the user is allowed to read this page.
isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
getPermissionErrors( $action, User $user, LinkTarget $page, $rigor=self::RIGOR_SECURE, $ignoreErrors=[])
Can $user perform $action on a page?
checkUserBlock( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Check that the user isn't blocked from editing.
invalidateUsersRightsCache( $user=null)
Clears users permissions cache, if specific user is provided it tries to clear permissions cache only...
getGroupPermissions( $groups)
Get the permissions associated with a given list of groups.
overrideUserRightsForTesting( $user, $rights=[])
Overrides user permissions cache.
groupHasPermission( $group, $role)
Check, if the given group has the given permission.
checkSpecialsAndNSPermissions( $action, UserIdentity $user, $errors, $rigor, $short, LinkTarget $page)
Check permissions on special pages & namespaces.
GroupPermissionsLookup $groupPermissionsLookup
addTemporaryUserRights(UserIdentity $user, $rights)
Add temporary user rights, only valid for the current scope.
getPermissionErrorsInternal( $action, User $user, LinkTarget $page, $rigor=self::RIGOR_SECURE, $short=false)
Can $user perform $action on a page? This is an internal function, with multiple levels of checks dep...
getGroupsWithPermission( $role)
Get all the groups who have a given permission.
userCan( $action, User $user, LinkTarget $page, $rigor=self::RIGOR_SECURE)
Can $user perform $action on a page?
checkActionPermissions( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Check action permissions not already checked in checkQuickPermissions.
getUserPermissions(UserIdentity $user)
Get the permissions this user has.
checkSiteConfigPermissions( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Check sitewide CSS/JSON/JS permissions.
checkCascadingSourcesRestrictions( $action, UserIdentity $user, $errors, $rigor, $short, LinkTarget $page)
Check restrictions on cascading pages.
userHasAnyRight(UserIdentity $user,... $actions)
Check if user is allowed to make any action.
getAllPermissions()
Get a list of all available permissions.
userCanEditRawHtmlPage(UserIdentity $user)
Check if user is allowed to edit sitewide pages that contain raw HTML.
__construct(ServiceOptions $options, SpecialPageFactory $specialPageFactory, NamespaceInfo $nsInfo, GroupPermissionsLookup $groupPermissionsLookup, UserGroupManager $userGroupManager, BlockErrorFormatter $blockErrorFormatter, HookContainer $hookContainer, UserCache $userCache, RedirectLookup $redirectLookup, RestrictionStore $restrictionStore, TitleFormatter $titleFormatter, TempUserConfig $tempUserConfig, UserFactory $userFactory)
isNamespaceProtected( $index, UserIdentity $user)
Determines if $user is unable to edit pages in namespace because it has been protected.
userHasRight(UserIdentity $user, $action='')
Testing a permission.
$coreRights
Array of Strings Core rights.
getNamespaceRestrictionLevels( $index, UserIdentity $user=null)
Determine which restriction levels it makes sense to use in a namespace, optionally filtered by a use...
isSameSpecialPage( $name, LinkTarget $page)
Returns true if this title resolves to the named special page.
checkQuickPermissions( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Permissions checks that fail most often, and which are easiest to test.
resultToError( $errors, $result)
Add the resulting error code to the errors array.
isBlockedFrom(User $user, $page, $fromReplica=false)
Check if user is blocked from editing a particular article.
string[][][] $temporaryUserRights
Temporary user rights, valid for the current request only.
string[] null $allRights
Cached results of getAllPermissions()
checkPageRestrictions( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Check against page_restrictions table requirements on this page.
userHasAllRights(UserIdentity $user,... $actions)
Check if user is allowed to make all actions.
This serves as the entry point to the MediaWiki session handling system.
static getGlobalSession()
If PHP's session_id() has been set, returns that session.
Factory for handling the special page list and generating SpecialPage objects.
Creates User objects.
Definition: UserFactory.php:38
string null $action
Cache what action this request is.
Definition: MediaWiki.php:47
IContextSource $context
Definition: MediaWiki.php:42
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Group all the pieces relevant to the context of a request into one instance.
static getMain()
Get the RequestContext object associated with the main request.
Parent class for all special pages.
Definition: SpecialPage.php:44
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Represents a title within MediaWiki.
Definition: Title.php:48
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:281
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:305
static castFromPageIdentity(?PageIdentity $pageIdentity)
Return a Title for a given PageIdentity.
Definition: Title.php:318
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
getBlock( $freshness=self::READ_NORMAL, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
Definition: User.php:1742
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:2651
isRegistered()
Get whether the user is registered.
Definition: User.php:2547
isEmailConfirmed()
Is this user's e-mail address valid-looking and confirmed within limits of the current site configura...
Definition: User.php:3453
isHidden()
Check if user account is hidden.
Definition: User.php:1876
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition: User.php:676
isNamed()
Is the user a normal non-temporary registered user?
Definition: User.php:3796
isTemp()
Is the user an autocreated temporary user?
Definition: User.php:3783
getTalkPage()
Get this user's talk page title.
Definition: User.php:3158
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:3635
isAnon()
Get whether the user is anonymous.
Definition: User.php:2555
getNamespace()
Get the namespace index.
getDBkey()
Get the main part of the link target, in canonical database form.
Interface for objects (potentially) representing an editable wiki page.
Service for resolving a wiki page redirect.
Interface for temporary user creation config and name matching.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
A title formatter service for MediaWiki.