MediaWiki REL1_34
PermissionManager.php
Go to the documentation of this file.
1<?php
21
22use Action;
23use Exception;
24use Hooks;
35use SpecialPage;
36use Title;
37use User;
38use Wikimedia\ScopedCallback;
39use WikiPage;
40
48
50 const RIGOR_QUICK = 'quick';
51
53 const RIGOR_FULL = 'full';
54
56 const RIGOR_SECURE = 'secure';
57
62 public const CONSTRUCTOR_OPTIONS = [
63 'WhitelistRead',
64 'WhitelistReadRegexp',
65 'EmailConfirmToEdit',
66 'BlockDisablesLogin',
67 'GroupPermissions',
68 'RevokePermissions',
69 'AvailableRights',
70 'NamespaceProtection',
71 'RestrictionLevels'
72 ];
73
75 private $options;
76
79
82
84 private $nsInfo;
85
87 private $allRights;
88
90 private $usersRights = null;
91
97
99 private $cachedRights = [];
100
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
196 public function __construct(
201 ) {
202 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
203 $this->options = $options;
204 $this->specialPageFactory = $specialPageFactory;
205 $this->revisionLookup = $revisionLookup;
206 $this->nsInfo = $nsInfo;
207 }
208
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
247 public function quickUserCan( $action, User $user, LinkTarget $page ) {
248 return $this->userCan( $action, $user, $page, self::RIGOR_QUICK );
249 }
250
268 public function getPermissionErrors(
269 $action,
270 User $user,
271 LinkTarget $page,
272 $rigor = self::RIGOR_SECURE,
273 $ignoreErrors = []
274 ) {
275 $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
276
277 // Remove the errors being ignored.
278 foreach ( $errors as $index => $error ) {
279 $errKey = is_array( $error ) ? $error[0] : $error;
280
281 if ( in_array( $errKey, $ignoreErrors ) ) {
282 unset( $errors[$index] );
283 }
284 if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
285 unset( $errors[$index] );
286 }
287 }
288
289 return $errors;
290 }
291
302 public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
303 $block = $user->getBlock( $fromReplica );
304 if ( !$block ) {
305 return false;
306 }
307
308 // TODO: remove upon further migration to LinkTarget
309 $title = Title::newFromLinkTarget( $page );
310
311 $blocked = $user->isHidden();
312 if ( !$blocked ) {
313 // Special handling for a user's own talk page. The block is not aware
314 // of the user, so this must be done here.
315 if ( $title->equals( $user->getTalkPage() ) ) {
316 $blocked = $block->appliesToUsertalk( $title );
317 } else {
318 $blocked = $block->appliesToTitle( $title );
319 }
320 }
321
322 // only for the purpose of the hook. We really don't need this here.
323 $allowUsertalk = $user->isAllowUsertalk();
324
325 // Allow extensions to let a blocked user access a particular page
326 Hooks::run( 'UserIsBlockedFrom', [ $user, $title, &$blocked, &$allowUsertalk ] );
327
328 return $blocked;
329 }
330
349 $action,
350 User $user,
351 LinkTarget $page,
352 $rigor = self::RIGOR_SECURE,
353 $short = false
354 ) {
355 if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
356 throw new Exception( "Invalid rigor parameter '$rigor'." );
357 }
358
359 # Read has special handling
360 if ( $action == 'read' ) {
361 $checks = [
362 'checkPermissionHooks',
363 'checkReadPermissions',
364 'checkUserBlock', // for wgBlockDisablesLogin
365 ];
366 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
367 # or checkUserConfigPermissions here as it will lead to duplicate
368 # error messages. This is okay to do since anywhere that checks for
369 # create will also check for edit, and those checks are called for edit.
370 } elseif ( $action == 'create' ) {
371 $checks = [
372 'checkQuickPermissions',
373 'checkPermissionHooks',
374 'checkPageRestrictions',
375 'checkCascadingSourcesRestrictions',
376 'checkActionPermissions',
377 'checkUserBlock'
378 ];
379 } else {
380 $checks = [
381 'checkQuickPermissions',
382 'checkPermissionHooks',
383 'checkSpecialsAndNSPermissions',
384 'checkSiteConfigPermissions',
385 'checkUserConfigPermissions',
386 'checkPageRestrictions',
387 'checkCascadingSourcesRestrictions',
388 'checkActionPermissions',
389 'checkUserBlock'
390 ];
391 }
392
393 $errors = [];
394 foreach ( $checks as $method ) {
395 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
396
397 if ( $short && $errors !== [] ) {
398 break;
399 }
400 }
401
402 return $errors;
403 }
404
421 private function checkPermissionHooks(
422 $action,
423 User $user,
424 $errors,
425 $rigor,
426 $short,
427 LinkTarget $page
428 ) {
429 // TODO: remove when LinkTarget usage will expand further
430 $title = Title::newFromLinkTarget( $page );
431 // Use getUserPermissionsErrors instead
432 $result = '';
433 if ( !Hooks::run( 'userCan', [ &$title, &$user, $action, &$result ] ) ) {
434 return $result ? [] : [ [ 'badaccess-group0' ] ];
435 }
436 // Check getUserPermissionsErrors hook
437 if ( !Hooks::run( 'getUserPermissionsErrors', [ &$title, &$user, $action, &$result ] ) ) {
438 $errors = $this->resultToError( $errors, $result );
439 }
440 // Check getUserPermissionsErrorsExpensive hook
441 if (
442 $rigor !== self::RIGOR_QUICK
443 && !( $short && count( $errors ) > 0 )
444 && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$title, &$user, $action, &$result ] )
445 ) {
446 $errors = $this->resultToError( $errors, $result );
447 }
448
449 return $errors;
450 }
451
460 private function resultToError( $errors, $result ) {
461 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
462 // A single array representing an error
463 $errors[] = $result;
464 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
465 // A nested array representing multiple errors
466 $errors = array_merge( $errors, $result );
467 } elseif ( $result !== '' && is_string( $result ) ) {
468 // A string representing a message-id
469 $errors[] = [ $result ];
470 } elseif ( $result instanceof MessageSpecifier ) {
471 // A message specifier representing an error
472 $errors[] = [ $result ];
473 } elseif ( $result === false ) {
474 // a generic "We don't want them to do that"
475 $errors[] = [ 'badaccess-group0' ];
476 }
477 return $errors;
478 }
479
496 private function checkReadPermissions(
497 $action,
498 User $user,
499 $errors,
500 $rigor,
501 $short,
502 LinkTarget $page
503 ) {
504 // TODO: remove when LinkTarget usage will expand further
505 $title = Title::newFromLinkTarget( $page );
506
507 $whiteListRead = $this->options->get( 'WhitelistRead' );
508 $whitelisted = false;
509 if ( $this->isEveryoneAllowed( 'read' ) ) {
510 # Shortcut for public wikis, allows skipping quite a bit of code
511 $whitelisted = true;
512 } elseif ( $this->userHasRight( $user, 'read' ) ) {
513 # If the user is allowed to read pages, he is allowed to read all pages
514 $whitelisted = true;
515 } elseif ( $this->isSameSpecialPage( 'Userlogin', $title )
516 || $this->isSameSpecialPage( 'PasswordReset', $title )
517 || $this->isSameSpecialPage( 'Userlogout', $title )
518 ) {
519 # Always grant access to the login page.
520 # Even anons need to be able to log in.
521 $whitelisted = true;
522 } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
523 # Time to check the whitelist
524 # Only do these checks is there's something to check against
525 $name = $title->getPrefixedText();
526 $dbName = $title->getPrefixedDBkey();
527
528 // Check for explicit whitelisting with and without underscores
529 if ( in_array( $name, $whiteListRead, true )
530 || in_array( $dbName, $whiteListRead, true ) ) {
531 $whitelisted = true;
532 } elseif ( $title->getNamespace() == NS_MAIN ) {
533 # Old settings might have the title prefixed with
534 # a colon for main-namespace pages
535 if ( in_array( ':' . $name, $whiteListRead ) ) {
536 $whitelisted = true;
537 }
538 } elseif ( $title->isSpecialPage() ) {
539 # If it's a special page, ditch the subpage bit and check again
540 $name = $title->getDBkey();
541 list( $name, /* $subpage */ ) =
542 $this->specialPageFactory->resolveAlias( $name );
543 if ( $name ) {
544 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
545 if ( in_array( $pure, $whiteListRead, true ) ) {
546 $whitelisted = true;
547 }
548 }
549 }
550 }
551
552 $whitelistReadRegexp = $this->options->get( 'WhitelistReadRegexp' );
553 if ( !$whitelisted && is_array( $whitelistReadRegexp )
554 && !empty( $whitelistReadRegexp ) ) {
555 $name = $title->getPrefixedText();
556 // Check for regex whitelisting
557 foreach ( $whitelistReadRegexp as $listItem ) {
558 if ( preg_match( $listItem, $name ) ) {
559 $whitelisted = true;
560 break;
561 }
562 }
563 }
564
565 if ( !$whitelisted ) {
566 # If the title is not whitelisted, give extensions a chance to do so...
567 Hooks::run( 'TitleReadWhitelist', [ $title, $user, &$whitelisted ] );
568 if ( !$whitelisted ) {
569 $errors[] = $this->missingPermissionError( $action, $short );
570 }
571 }
572
573 return $errors;
574 }
575
584 private function missingPermissionError( $action, $short ) {
585 // We avoid expensive display logic for quickUserCan's and such
586 if ( $short ) {
587 return [ 'badaccess-group0' ];
588 }
589
590 // TODO: it would be a good idea to replace the method below with something else like
591 // maybe callback injection
592 return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
593 }
594
603 private function isSameSpecialPage( $name, LinkTarget $page ) {
604 if ( $page->getNamespace() == NS_SPECIAL ) {
605 list( $thisName, /* $subpage */ ) =
606 $this->specialPageFactory->resolveAlias( $page->getDBkey() );
607 if ( $name == $thisName ) {
608 return true;
609 }
610 }
611 return false;
612 }
613
630 private function checkUserBlock(
631 $action,
632 User $user,
633 $errors,
634 $rigor,
635 $short,
636 LinkTarget $page
637 ) {
638 // Account creation blocks handled at userlogin.
639 // Unblocking handled in SpecialUnblock
640 if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
641 return $errors;
642 }
643
644 // Optimize for a very common case
645 if ( $action === 'read' && !$this->options->get( 'BlockDisablesLogin' ) ) {
646 return $errors;
647 }
648
649 if ( $this->options->get( 'EmailConfirmToEdit' )
650 && !$user->isEmailConfirmed()
651 && $action === 'edit'
652 ) {
653 $errors[] = [ 'confirmedittext' ];
654 }
655
656 $useReplica = ( $rigor !== self::RIGOR_SECURE );
657 $block = $user->getBlock( $useReplica );
658
659 // If the user does not have a block, or the block they do have explicitly
660 // allows the action (like "read" or "upload").
661 if ( !$block || $block->appliesToRight( $action ) === false ) {
662 return $errors;
663 }
664
665 // Determine if the user is blocked from this action on this page.
666 // What gets passed into this method is a user right, not an action name.
667 // There is no way to instantiate an action by restriction. However, this
668 // will get the action where the restriction is the same. This may result
669 // in actions being blocked that shouldn't be.
670 $actionObj = null;
671 if ( Action::exists( $action ) ) {
672 // TODO: this drags a ton of dependencies in, would be good to avoid WikiPage
673 // instantiation and decouple it creating an ActionPermissionChecker interface
674 $wikiPage = WikiPage::factory( Title::newFromLinkTarget( $page, 'clone' ) );
675 // Creating an action will perform several database queries to ensure that
676 // the action has not been overridden by the content type.
677 // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
678 // probably we may use fake context object since it's unlikely that Action uses it
679 // anyway. It would be nice if we could avoid instantiating the Action at all.
680 $actionObj = Action::factory( $action, $wikiPage, RequestContext::getMain() );
681 // Ensure that the retrieved action matches the restriction.
682 if ( $actionObj && $actionObj->getRestriction() !== $action ) {
683 $actionObj = null;
684 }
685 }
686
687 // If no action object is returned, assume that the action requires unblock
688 // which is the default.
689 if ( !$actionObj || $actionObj->requiresUnblock() ) {
690 if ( $this->isBlockedFrom( $user, $page, $useReplica ) ) {
691 // @todo FIXME: Pass the relevant context into this function.
692 $errors[] = $block->getPermissionsError( RequestContext::getMain() );
693 }
694 }
695
696 return $errors;
697 }
698
715 private function checkQuickPermissions(
716 $action,
717 User $user,
718 $errors,
719 $rigor,
720 $short,
721 LinkTarget $page
722 ) {
723 // TODO: remove when LinkTarget usage will expand further
724 $title = Title::newFromLinkTarget( $page );
725
726 if ( !Hooks::run( 'TitleQuickPermissions',
727 [ $title, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
728 ) {
729 return $errors;
730 }
731
732 $isSubPage = $this->nsInfo->hasSubpages( $title->getNamespace() ) ?
733 strpos( $title->getText(), '/' ) !== false : false;
734
735 if ( $action == 'create' ) {
736 if (
737 ( $this->nsInfo->isTalk( $title->getNamespace() ) &&
738 !$this->userHasRight( $user, 'createtalk' ) ) ||
739 ( !$this->nsInfo->isTalk( $title->getNamespace() ) &&
740 !$this->userHasRight( $user, 'createpage' ) )
741 ) {
742 $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
743 }
744 } elseif ( $action == 'move' ) {
745 if ( !$this->userHasRight( $user, 'move-rootuserpages' )
746 && $title->getNamespace() == NS_USER && !$isSubPage ) {
747 // Show user page-specific message only if the user can move other pages
748 $errors[] = [ 'cant-move-user-page' ];
749 }
750
751 // Check if user is allowed to move files if it's a file
752 if ( $title->getNamespace() == NS_FILE &&
753 !$this->userHasRight( $user, 'movefile' ) ) {
754 $errors[] = [ 'movenotallowedfile' ];
755 }
756
757 // Check if user is allowed to move category pages if it's a category page
758 if ( $title->getNamespace() == NS_CATEGORY &&
759 !$this->userHasRight( $user, 'move-categorypages' ) ) {
760 $errors[] = [ 'cant-move-category-page' ];
761 }
762
763 if ( !$this->userHasRight( $user, 'move' ) ) {
764 // User can't move anything
765 $userCanMove = $this->groupHasPermission( 'user', 'move' );
766 $autoconfirmedCanMove = $this->groupHasPermission( 'autoconfirmed', 'move' );
767 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
768 // custom message if logged-in users without any special rights can move
769 $errors[] = [ 'movenologintext' ];
770 } else {
771 $errors[] = [ 'movenotallowed' ];
772 }
773 }
774 } elseif ( $action == 'move-target' ) {
775 if ( !$this->userHasRight( $user, 'move' ) ) {
776 // User can't move anything
777 $errors[] = [ 'movenotallowed' ];
778 } elseif ( !$this->userHasRight( $user, 'move-rootuserpages' )
779 && $title->getNamespace() == NS_USER && !$isSubPage ) {
780 // Show user page-specific message only if the user can move other pages
781 $errors[] = [ 'cant-move-to-user-page' ];
782 } elseif ( !$this->userHasRight( $user, 'move-categorypages' )
783 && $title->getNamespace() == NS_CATEGORY ) {
784 // Show category page-specific message only if the user can move other pages
785 $errors[] = [ 'cant-move-to-category-page' ];
786 }
787 } elseif ( !$this->userHasRight( $user, $action ) ) {
788 $errors[] = $this->missingPermissionError( $action, $short );
789 }
790
791 return $errors;
792 }
793
812 private function checkPageRestrictions(
813 $action,
814 User $user,
815 $errors,
816 $rigor,
817 $short,
818 LinkTarget $page
819 ) {
820 // TODO: remove & rework upon further use of LinkTarget
821 $title = Title::newFromLinkTarget( $page );
822 foreach ( $title->getRestrictions( $action ) as $right ) {
823 // Backwards compatibility, rewrite sysop -> editprotected
824 if ( $right == 'sysop' ) {
825 $right = 'editprotected';
826 }
827 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
828 if ( $right == 'autoconfirmed' ) {
829 $right = 'editsemiprotected';
830 }
831 if ( $right == '' ) {
832 continue;
833 }
834 if ( !$this->userHasRight( $user, $right ) ) {
835 $errors[] = [ 'protectedpagetext', $right, $action ];
836 } elseif ( $title->areRestrictionsCascading() &&
837 !$this->userHasRight( $user, 'protect' ) ) {
838 $errors[] = [ 'protectedpagetext', 'protect', $action ];
839 }
840 }
841
842 return $errors;
843 }
844
862 $action,
863 UserIdentity $user,
864 $errors,
865 $rigor,
866 $short,
867 LinkTarget $page
868 ) {
869 // TODO: remove & rework upon further use of LinkTarget
870 $title = Title::newFromLinkTarget( $page );
871 if ( $rigor !== self::RIGOR_QUICK && !$title->isUserConfigPage() ) {
872 # We /could/ use the protection level on the source page, but it's
873 # fairly ugly as we have to establish a precedence hierarchy for pages
874 # included by multiple cascade-protected pages. So just restrict
875 # it to people with 'protect' permission, as they could remove the
876 # protection anyway.
877 list( $cascadingSources, $restrictions ) = $title->getCascadeProtectionSources();
878 # Cascading protection depends on more than this page...
879 # Several cascading protected pages may include this page...
880 # Check each cascading level
881 # This is only for protection restrictions, not for all actions
882 if ( isset( $restrictions[$action] ) ) {
883 foreach ( $restrictions[$action] as $right ) {
884 // Backwards compatibility, rewrite sysop -> editprotected
885 if ( $right == 'sysop' ) {
886 $right = 'editprotected';
887 }
888 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
889 if ( $right == 'autoconfirmed' ) {
890 $right = 'editsemiprotected';
891 }
892 if ( $right != '' && !$this->userHasAllRights( $user, 'protect', $right ) ) {
893 $wikiPages = '';
895 foreach ( $cascadingSources as $wikiPage ) {
896 $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
897 }
898 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
899 }
900 }
901 }
902 }
903
904 return $errors;
905 }
906
923 private function checkActionPermissions(
924 $action,
925 User $user,
926 $errors,
927 $rigor,
928 $short,
929 LinkTarget $page
930 ) {
932
933 // TODO: remove & rework upon further use of LinkTarget
934 $title = Title::newFromLinkTarget( $page );
935
936 if ( $action == 'protect' ) {
937 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
938 // If they can't edit, they shouldn't protect.
939 $errors[] = [ 'protect-cantedit' ];
940 }
941 } elseif ( $action == 'create' ) {
942 $title_protection = $title->getTitleProtection();
943 if ( $title_protection ) {
944 if ( $title_protection['permission'] == ''
945 || !$this->userHasRight( $user, $title_protection['permission'] )
946 ) {
947 $errors[] = [
948 'titleprotected',
949 // TODO: get rid of the User dependency
950 User::whoIs( $title_protection['user'] ),
951 $title_protection['reason']
952 ];
953 }
954 }
955 } elseif ( $action == 'move' ) {
956 // Check for immobile pages
957 if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
958 // Specific message for this case
959 $errors[] = [ 'immobile-source-namespace', $title->getNsText() ];
960 } elseif ( !$title->isMovable() ) {
961 // Less specific message for rarer cases
962 $errors[] = [ 'immobile-source-page' ];
963 }
964 } elseif ( $action == 'move-target' ) {
965 if ( !$this->nsInfo->isMovable( $title->getNamespace() ) ) {
966 $errors[] = [ 'immobile-target-namespace', $title->getNsText() ];
967 } elseif ( !$title->isMovable() ) {
968 $errors[] = [ 'immobile-target-page' ];
969 }
970 } elseif ( $action == 'delete' ) {
971 $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $title );
972 if ( !$tempErrors ) {
973 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
974 $user, $tempErrors, $rigor, true, $title );
975 }
976 if ( $tempErrors ) {
977 // If protection keeps them from editing, they shouldn't be able to delete.
978 $errors[] = [ 'deleteprotected' ];
979 }
980 if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit
981 && !$this->userCan( 'bigdelete', $user, $title ) && $title->isBigDeletion()
982 ) {
983 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
984 }
985 } elseif ( $action === 'undelete' ) {
986 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $title, $rigor, true ) ) ) {
987 // Undeleting implies editing
988 $errors[] = [ 'undelete-cantedit' ];
989 }
990 if ( !$title->exists()
991 && count( $this->getPermissionErrorsInternal( 'create', $user, $title, $rigor, true ) )
992 ) {
993 // Undeleting where nothing currently exists implies creating
994 $errors[] = [ 'undelete-cantcreate' ];
995 }
996 }
997 return $errors;
998 }
999
1017 $action,
1018 UserIdentity $user,
1019 $errors,
1020 $rigor,
1021 $short,
1022 LinkTarget $page
1023 ) {
1024 // TODO: remove & rework upon further use of LinkTarget
1025 $title = Title::newFromLinkTarget( $page );
1026
1027 # Only 'createaccount' can be performed on special pages,
1028 # which don't actually exist in the DB.
1029 if ( $title->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) {
1030 $errors[] = [ 'ns-specialprotected' ];
1031 }
1032
1033 # Check $wgNamespaceProtection for restricted namespaces
1034 if ( $this->isNamespaceProtected( $title->getNamespace(), $user ) ) {
1035 $ns = $title->getNamespace() == NS_MAIN ?
1036 wfMessage( 'nstab-main' )->text() : $title->getNsText();
1037 $errors[] = $title->getNamespace() == NS_MEDIAWIKI ?
1038 [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
1039 }
1040
1041 return $errors;
1042 }
1043
1061 $action,
1062 User $user,
1063 $errors,
1064 $rigor,
1065 $short,
1066 LinkTarget $page
1067 ) {
1068 // TODO: remove & rework upon further use of LinkTarget
1069 $title = Title::newFromLinkTarget( $page );
1070
1071 if ( $action != 'patrol' ) {
1072 $error = null;
1073 // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
1074 // editinterface right. That's implemented as a restriction so no check needed here.
1075 if ( $title->isSiteCssConfigPage() && !$this->userHasRight( $user, 'editsitecss' ) ) {
1076 $error = [ 'sitecssprotected', $action ];
1077 } elseif ( $title->isSiteJsonConfigPage() && !$this->userHasRight( $user, 'editsitejson' ) ) {
1078 $error = [ 'sitejsonprotected', $action ];
1079 } elseif ( $title->isSiteJsConfigPage() && !$this->userHasRight( $user, 'editsitejs' ) ) {
1080 $error = [ 'sitejsprotected', $action ];
1081 } elseif ( $title->isRawHtmlMessage() ) {
1082 // Raw HTML can be used to deploy CSS or JS so require rights for both.
1083 if ( !$this->userHasRight( $user, 'editsitejs' ) ) {
1084 $error = [ 'sitejsprotected', $action ];
1085 } elseif ( !$this->userHasRight( $user, 'editsitecss' ) ) {
1086 $error = [ 'sitecssprotected', $action ];
1087 }
1088 }
1089
1090 if ( $error ) {
1091 if ( $this->userHasRight( $user, 'editinterface' ) ) {
1092 // Most users / site admins will probably find out about the new, more restrictive
1093 // permissions by failing to edit something. Give them more info.
1094 // TODO remove this a few release cycles after 1.32
1095 $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
1096 }
1097 $errors[] = $error;
1098 }
1099 }
1100
1101 return $errors;
1102 }
1103
1121 $action,
1122 UserIdentity $user,
1123 $errors,
1124 $rigor,
1125 $short,
1126 LinkTarget $page
1127 ) {
1128 // TODO: remove & rework upon further use of LinkTarget
1129 $title = Title::newFromLinkTarget( $page );
1130
1131 # Protect css/json/js subpages of user pages
1132 # XXX: this might be better using restrictions
1133
1134 if ( $action === 'patrol' ) {
1135 return $errors;
1136 }
1137
1138 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $title->getText() ) ) {
1139 // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1140 if (
1141 $title->isUserCssConfigPage()
1142 && !$this->userHasAnyRight( $user, 'editmyusercss', 'editusercss' )
1143 ) {
1144 $errors[] = [ 'mycustomcssprotected', $action ];
1145 } elseif (
1146 $title->isUserJsonConfigPage()
1147 && !$this->userHasAnyRight( $user, 'editmyuserjson', 'edituserjson' )
1148 ) {
1149 $errors[] = [ 'mycustomjsonprotected', $action ];
1150 } elseif (
1151 $title->isUserJsConfigPage()
1152 && !$this->userHasAnyRight( $user, 'editmyuserjs', 'edituserjs' )
1153 ) {
1154 $errors[] = [ 'mycustomjsprotected', $action ];
1155 } elseif (
1156 $title->isUserJsConfigPage()
1157 && !$this->userHasAnyRight( $user, 'edituserjs', 'editmyuserjsredirect' )
1158 ) {
1159 // T207750 - do not allow users to edit a redirect if they couldn't edit the target
1160 $rev = $this->revisionLookup->getRevisionByTitle( $title );
1161 $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null;
1162 $target = $content ? $content->getUltimateRedirectTarget() : null;
1163 if ( $target && (
1164 !$target->inNamespace( NS_USER )
1165 || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
1166 ) ) {
1167 $errors[] = [ 'mycustomjsredirectprotected', $action ];
1168 }
1169 }
1170 } else {
1171 // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
1172 // deletion/suppression which cannot be used for attacks and we want to avoid the
1173 // situation where an unprivileged user can post abusive content on their subpages
1174 // and only very highly privileged users could remove it.
1175 if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
1176 if (
1177 $title->isUserCssConfigPage()
1178 && !$this->userHasRight( $user, 'editusercss' )
1179 ) {
1180 $errors[] = [ 'customcssprotected', $action ];
1181 } elseif (
1182 $title->isUserJsonConfigPage()
1183 && !$this->userHasRight( $user, 'edituserjson' )
1184 ) {
1185 $errors[] = [ 'customjsonprotected', $action ];
1186 } elseif (
1187 $title->isUserJsConfigPage()
1188 && !$this->userHasRight( $user, 'edituserjs' )
1189 ) {
1190 $errors[] = [ 'customjsprotected', $action ];
1191 }
1192 }
1193 }
1194
1195 return $errors;
1196 }
1197
1208 public function userHasRight( UserIdentity $user, $action = '' ) {
1209 if ( $action === '' ) {
1210 return true; // In the spirit of DWIM
1211 }
1212 // Use strict parameter to avoid matching numeric 0 accidentally inserted
1213 // by misconfiguration: 0 == 'foo'
1214 return in_array( $action, $this->getUserPermissions( $user ), true );
1215 }
1216
1226 public function userHasAnyRight( UserIdentity $user ) {
1227 $actions = array_slice( func_get_args(), 1 );
1228 foreach ( $actions as $action ) {
1229 if ( $this->userHasRight( $user, $action ) ) {
1230 return true;
1231 }
1232 }
1233 return false;
1234 }
1235
1245 public function userHasAllRights( UserIdentity $user ) {
1246 $actions = array_slice( func_get_args(), 1 );
1247 foreach ( $actions as $action ) {
1248 if ( !$this->userHasRight( $user, $action ) ) {
1249 return false;
1250 }
1251 }
1252 return true;
1253 }
1254
1264 public function getUserPermissions( UserIdentity $user ) {
1265 $user = User::newFromIdentity( $user );
1266 $rightsCacheKey = $this->getRightsCacheKey( $user );
1267 if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1268 $this->usersRights[ $rightsCacheKey ] = $this->getGroupPermissions(
1269 $user->getEffectiveGroups()
1270 );
1271 Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $rightsCacheKey ] ] );
1272
1273 // Deny any rights denied by the user's session, unless this
1274 // endpoint has no sessions.
1275 if ( !defined( 'MW_NO_SESSION' ) ) {
1276 // FIXME: $user->getRequest().. need to be replaced with something else
1277 $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
1278 if ( $allowedRights !== null ) {
1279 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1280 $this->usersRights[ $rightsCacheKey ],
1281 $allowedRights
1282 );
1283 }
1284 }
1285
1286 Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $rightsCacheKey ] ] );
1287 // Force reindexation of rights when a hook has unset one of them
1288 $this->usersRights[ $rightsCacheKey ] = array_values(
1289 array_unique( $this->usersRights[ $rightsCacheKey ] )
1290 );
1291
1292 if (
1293 $user->isLoggedIn() &&
1294 $this->options->get( 'BlockDisablesLogin' ) &&
1295 $user->getBlock()
1296 ) {
1297 $anon = new User;
1298 $this->usersRights[ $rightsCacheKey ] = array_intersect(
1299 $this->usersRights[ $rightsCacheKey ],
1300 $this->getUserPermissions( $anon )
1301 );
1302 }
1303 }
1304 $rights = $this->usersRights[ $rightsCacheKey ];
1305 foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) {
1306 $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
1307 }
1308 return $rights;
1309 }
1310
1319 public function invalidateUsersRightsCache( $user = null ) {
1320 if ( $user !== null ) {
1321 $rightsCacheKey = $this->getRightsCacheKey( $user );
1322 if ( isset( $this->usersRights[ $rightsCacheKey ] ) ) {
1323 unset( $this->usersRights[ $rightsCacheKey ] );
1324 }
1325 } else {
1326 $this->usersRights = null;
1327 }
1328 }
1329
1335 private function getRightsCacheKey( UserIdentity $user ) {
1336 return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
1337 }
1338
1353 public function groupHasPermission( $group, $role ) {
1354 $groupPermissions = $this->options->get( 'GroupPermissions' );
1355 $revokePermissions = $this->options->get( 'RevokePermissions' );
1356 return isset( $groupPermissions[$group][$role] ) && $groupPermissions[$group][$role] &&
1357 !( isset( $revokePermissions[$group][$role] ) && $revokePermissions[$group][$role] );
1358 }
1359
1368 public function getGroupPermissions( $groups ) {
1369 $rights = [];
1370 // grant every granted permission first
1371 foreach ( $groups as $group ) {
1372 if ( isset( $this->options->get( 'GroupPermissions' )[$group] ) ) {
1373 $rights = array_merge( $rights,
1374 // array_filter removes empty items
1375 array_keys( array_filter( $this->options->get( 'GroupPermissions' )[$group] ) ) );
1376 }
1377 }
1378 // now revoke the revoked permissions
1379 foreach ( $groups as $group ) {
1380 if ( isset( $this->options->get( 'RevokePermissions' )[$group] ) ) {
1381 $rights = array_diff( $rights,
1382 array_keys( array_filter( $this->options->get( 'RevokePermissions' )[$group] ) ) );
1383 }
1384 }
1385 return array_unique( $rights );
1386 }
1387
1396 public function getGroupsWithPermission( $role ) {
1397 $allowedGroups = [];
1398 foreach ( array_keys( $this->options->get( 'GroupPermissions' ) ) as $group ) {
1399 if ( $this->groupHasPermission( $group, $role ) ) {
1400 $allowedGroups[] = $group;
1401 }
1402 }
1403 return $allowedGroups;
1404 }
1405
1421 public function isEveryoneAllowed( $right ) {
1422 // Use the cached results, except in unit tests which rely on
1423 // being able change the permission mid-request
1424 if ( isset( $this->cachedRights[$right] ) ) {
1425 return $this->cachedRights[$right];
1426 }
1427
1428 if ( !isset( $this->options->get( 'GroupPermissions' )['*'][$right] )
1429 || !$this->options->get( 'GroupPermissions' )['*'][$right] ) {
1430 $this->cachedRights[$right] = false;
1431 return false;
1432 }
1433
1434 // If it's revoked anywhere, then everyone doesn't have it
1435 foreach ( $this->options->get( 'RevokePermissions' ) as $rights ) {
1436 if ( isset( $rights[$right] ) && $rights[$right] ) {
1437 $this->cachedRights[$right] = false;
1438 return false;
1439 }
1440 }
1441
1442 // Remove any rights that aren't allowed to the global-session user,
1443 // unless there are no sessions for this endpoint.
1444 if ( !defined( 'MW_NO_SESSION' ) ) {
1445
1446 // XXX: think what could be done with the below
1447 $allowedRights = SessionManager::getGlobalSession()->getAllowedUserRights();
1448 if ( $allowedRights !== null && !in_array( $right, $allowedRights, true ) ) {
1449 $this->cachedRights[$right] = false;
1450 return false;
1451 }
1452 }
1453
1454 // Allow extensions to say false
1455 if ( !Hooks::run( 'UserIsEveryoneAllowed', [ $right ] ) ) {
1456 $this->cachedRights[$right] = false;
1457 return false;
1458 }
1459
1460 $this->cachedRights[$right] = true;
1461 return true;
1462 }
1463
1471 public function getAllPermissions() {
1472 if ( $this->allRights === null ) {
1473 if ( count( $this->options->get( 'AvailableRights' ) ) ) {
1474 $this->allRights = array_unique( array_merge(
1475 $this->coreRights,
1476 $this->options->get( 'AvailableRights' )
1477 ) );
1478 } else {
1479 $this->allRights = $this->coreRights;
1480 }
1481 Hooks::run( 'UserGetAllRights', [ &$this->allRights ] );
1482 }
1483 return $this->allRights;
1484 }
1485
1492 private function isNamespaceProtected( $index, UserIdentity $user ) {
1493 $namespaceProtection = $this->options->get( 'NamespaceProtection' );
1494 if ( isset( $namespaceProtection[$index] ) ) {
1495 return !$this->userHasAllRights( $user, ...(array)$namespaceProtection[$index] );
1496 }
1497 return false;
1498 }
1499
1508 public function getNamespaceRestrictionLevels( $index, UserIdentity $user = null ) {
1509 if ( !isset( $this->options->get( 'NamespaceProtection' )[$index] ) ) {
1510 // All levels are valid if there's no namespace restriction.
1511 // But still filter by user, if necessary
1512 $levels = $this->options->get( 'RestrictionLevels' );
1513 if ( $user ) {
1514 $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
1515 $right = $level;
1516 if ( $right == 'sysop' ) {
1517 $right = 'editprotected'; // BC
1518 }
1519 if ( $right == 'autoconfirmed' ) {
1520 $right = 'editsemiprotected'; // BC
1521 }
1522 return $this->userHasRight( $user, $right );
1523 } ) );
1524 }
1525 return $levels;
1526 }
1527
1528 // $wgNamespaceProtection can require one or more rights to edit the namespace, which
1529 // may be satisfied by membership in multiple groups each giving a subset of those rights.
1530 // A restriction level is redundant if, for any one of the namespace rights, all groups
1531 // giving that right also give the restriction level's right. Or, conversely, a
1532 // restriction level is not redundant if, for every namespace right, there's at least one
1533 // group giving that right without the restriction level's right.
1534 //
1535 // First, for each right, get a list of groups with that right.
1536 $namespaceRightGroups = [];
1537 foreach ( (array)$this->options->get( 'NamespaceProtection' )[$index] as $right ) {
1538 if ( $right == 'sysop' ) {
1539 $right = 'editprotected'; // BC
1540 }
1541 if ( $right == 'autoconfirmed' ) {
1542 $right = 'editsemiprotected'; // BC
1543 }
1544 if ( $right != '' ) {
1545 $namespaceRightGroups[$right] = $this->getGroupsWithPermission( $right );
1546 }
1547 }
1548
1549 // Now, go through the protection levels one by one.
1550 $usableLevels = [ '' ];
1551 foreach ( $this->options->get( 'RestrictionLevels' ) as $level ) {
1552 $right = $level;
1553 if ( $right == 'sysop' ) {
1554 $right = 'editprotected'; // BC
1555 }
1556 if ( $right == 'autoconfirmed' ) {
1557 $right = 'editsemiprotected'; // BC
1558 }
1559
1560 if ( $right != '' &&
1561 !isset( $namespaceRightGroups[$right] ) &&
1562 ( !$user || $this->userHasRight( $user, $right ) )
1563 ) {
1564 // Do any of the namespace rights imply the restriction right? (see explanation above)
1565 foreach ( $namespaceRightGroups as $groups ) {
1566 if ( !array_diff( $groups, $this->getGroupsWithPermission( $right ) ) ) {
1567 // Yes, this one does.
1568 continue 2;
1569 }
1570 }
1571 // No, keep the restriction level
1572 $usableLevels[] = $level;
1573 }
1574 }
1575
1576 return $usableLevels;
1577 }
1578
1593 public function addTemporaryUserRights( UserIdentity $user, $rights ) {
1594 $userId = $user->getId();
1595 $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
1596 $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
1597 return new ScopedCallback( function () use ( $userId, $nextKey ) {
1598 unset( $this->temporaryUserRights[$userId][$nextKey] );
1599 } );
1600 }
1601
1612 public function overrideUserRightsForTesting( $user, $rights = [] ) {
1613 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1614 throw new Exception( __METHOD__ . ' can not be called outside of tests' );
1615 }
1616 $this->usersRights[ $this->getRightsCacheKey( $user ) ] =
1617 is_array( $rights ) ? $rights : [ $rights ];
1618 }
1619
1620}
$wgDeleteRevisionsLimit
Optional to restrict deletion of pages with higher revision counts to users with the 'bigdelete' perm...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$wgLang
Definition Setup.php:880
Actions are things which can be done to pages (edit, delete, rollback, etc).
Definition Action.php:39
static factory( $action, Page $page, IContextSource $context=null)
Get an appropriate Action subclass for the given action.
Definition Action.php:97
static exists( $name)
Check if a given action is recognised, even if it's disabled.
Definition Action.php:170
Hooks class.
Definition Hooks.php:34
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
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.
__construct(ServiceOptions $options, SpecialPageFactory $specialPageFactory, RevisionLookup $revisionLookup, NamespaceInfo $nsInfo)
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?
userHasAnyRight(UserIdentity $user)
Check if user is allowed to make any action.
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.
userHasAllRights(UserIdentity $user)
Check if user is allowed to make all actions.
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.
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.
getAllPermissions()
Get a list of all available permissions.
isBlockedFrom(User $user, LinkTarget $page, $fromReplica=false)
Check if user is blocked from editing a particular article.
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.
string[][][] $temporaryUserRights
Temporary user rights, valid for the current request only.
string[] null $allRights
Cached results of getAllRights()
checkPageRestrictions( $action, User $user, $errors, $rigor, $short, LinkTarget $page)
Check against page_restrictions table requirements on this page.
Page revision base class.
This serves as the entry point to the MediaWiki session handling system.
Factory for handling the special page list and generating SpecialPage objects.
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.
Parent class for all special pages.
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:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
isAllowUsertalk()
Checks if usertalk is allowed.
Definition User.php:5446
isEmailConfirmed()
Is this user's e-mail address valid-looking and confirmed within limits of the current site configura...
Definition User.php:4740
isHidden()
Check if user account is hidden.
Definition User.php:2318
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:851
static newFromIdentity(UserIdentity $identity)
Returns a User object corresponding to the given UserIdentity.
Definition User.php:574
getTalkPage()
Get this user's talk page title.
Definition User.php:4390
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:5390
getBlock( $fromReplica=true)
Get the block affecting the user, or null if the user is not blocked.
Definition User.php:2200
isAnon()
Get whether the user is anonymous.
Definition User.php:3638
Class representing a MediaWiki article and history.
Definition WikiPage.php:47
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition WikiPage.php:142
const NS_USER
Definition Defines.php:71
const NS_FILE
Definition Defines.php:75
const NS_MAIN
Definition Defines.php:69
const NS_MEDIAWIKI
Definition Defines.php:77
const NS_SPECIAL
Definition Defines.php:58
const NS_CATEGORY
Definition Defines.php:83
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
Service for looking up page revisions.
Interface for objects representing user identity.
$content
Definition router.php:78