Awight has uploaded a new change for review. https://gerrit.wikimedia.org/r/166357
Change subject: WIP componentize Title permissions logic ...................................................................... WIP componentize Title permissions logic Move Title code dealing with permissions and restrictions into the new security component. See https://www.mediawiki.org/wiki/Requests_for_comment/Page_protection_as_a_component TODO: * Decide whether deprecated public properties are an issue. * Verify test coverage. Change-Id: Ifa5671f5e4cf16cf5447bf132b5081af95e543ee --- M includes/AutoLoader.php M includes/Title.php A includes/security/TitleSecurity.php 3 files changed, 1,260 insertions(+), 987 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/57/166357/1 diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 2a45fc3..61ba642 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -168,6 +168,7 @@ 'TitleArray' => 'includes/TitleArray.php', 'TitleArrayFromResult' => 'includes/TitleArrayFromResult.php', 'TitlePrefixSearch' => 'includes/PrefixSearch.php', + 'TitleSecurity' => 'includes/security/TitleSecurity.php', 'UploadSourceAdapter' => 'includes/Import.php', 'UppercaseCollation' => 'includes/Collation.php', 'User' => 'includes/User.php', diff --git a/includes/Title.php b/includes/Title.php index e8cda85..85f8c03 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -68,6 +68,9 @@ /** @var string Database key with the initial letter in the case specified by the user */ protected $mUserCaseDBKey; + /** @var string Text form including namespace/interwiki, initialised on demand */ + protected $mPrefixedText; + /** @var int Namespace index, i.e. one of the NS_xxxx constants */ public $mNamespace = NS_MAIN; @@ -94,36 +97,6 @@ /** @var int Estimated number of revisions; null of not loaded */ private $mEstimateRevisions; - - /** @var array Array of groups allowed to edit this article */ - public $mRestrictions = array(); - - /** @var bool */ - protected $mOldRestrictions = false; - - /** @var bool Cascade restrictions on this page to included templates and images? */ - public $mCascadeRestriction; - - /** Caching the results of getCascadeProtectionSources */ - public $mCascadingRestrictions; - - /** @var array When do the restrictions on this page expire? */ - protected $mRestrictionsExpiry = array(); - - /** @var bool Are cascading restrictions in effect on this page? */ - protected $mHasCascadingRestrictions; - - /** @var array Where are the cascading restrictions coming from on this page? */ - public $mCascadeSources; - - /** @var bool Boolean for initialisation on demand */ - public $mRestrictionsLoaded = false; - - /** @var string Text form including namespace/interwiki, initialised on demand */ - protected $mPrefixedText = null; - - /** @var mixed Cached value for getTitleProtection (create protection) */ - public $mTitleProtection; /** * @var int Namespace index when there is no namespace. Don't change the @@ -161,6 +134,9 @@ /** @var bool Would deleting this page be a big deletion? */ private $mIsBigDeletion = null; + + /** @var TitleSecurity manages protection and restrictions */ + protected $mTitleSecurity; // @} /** @@ -1869,7 +1845,7 @@ * @return bool */ public function quickUserCan( $action, $user = null ) { - return $this->userCan( $action, $user, false ); + return $this->getTitleSecurity()->quickUserCan( $this, $action, $user ); } /** @@ -1883,13 +1859,7 @@ * @return bool */ public function userCan( $action, $user = null, $doExpensiveQueries = true ) { - if ( !$user instanceof User ) { - global $wgUser; - $user = $wgUser; - } - - return !count( $this->getUserPermissionsErrorsInternal( - $action, $user, $doExpensiveQueries, true ) ); + return $this->getTitleSecurity()->userCan( $this, $action, $user, $doExpensiveQueries ); } /** @@ -1908,579 +1878,9 @@ public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { - $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); - - // Remove the errors being ignored. - foreach ( $errors as $index => $error ) { - $error_key = is_array( $error ) ? $error[0] : $error; - - if ( in_array( $error_key, $ignoreErrors ) ) { - unset( $errors[$index] ); - } - } - - return $errors; - } - - /** - * Permissions checks that fail most often, and which are easiest to test. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkQuickPermissions( $action, $user, $errors, - $doExpensiveQueries, $short - ) { - if ( !wfRunHooks( 'TitleQuickPermissions', - array( $this, $user, $action, &$errors, $doExpensiveQueries, $short ) ) - ) { - return $errors; - } - - if ( $action == 'create' ) { - if ( - ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || - ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) - ) { - $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); - } - } elseif ( $action == 'move' ) { - if ( !$user->isAllowed( 'move-rootuserpages' ) - && $this->mNamespace == NS_USER && !$this->isSubpage() ) { - // Show user page-specific message only if the user can move other pages - $errors[] = array( 'cant-move-user-page' ); - } - - // Check if user is allowed to move files if it's a file - if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { - $errors[] = array( 'movenotallowedfile' ); - } - - // Check if user is allowed to move category pages if it's a category page - if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) { - $errors[] = array( 'cant-move-category-page' ); - } - - if ( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $userCanMove = User::groupHasPermission( 'user', 'move' ); - $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); - if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { - // custom message if logged-in users without any special rights can move - $errors[] = array( 'movenologintext' ); - } else { - $errors[] = array( 'movenotallowed' ); - } - } - } elseif ( $action == 'move-target' ) { - if ( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $errors[] = array( 'movenotallowed' ); - } elseif ( !$user->isAllowed( 'move-rootuserpages' ) - && $this->mNamespace == NS_USER && !$this->isSubpage() ) { - // Show user page-specific message only if the user can move other pages - $errors[] = array( 'cant-move-to-user-page' ); - } elseif ( !$user->isAllowed( 'move-categorypages' ) - && $this->mNamespace == NS_CATEGORY ) { - // Show category page-specific message only if the user can move other pages - $errors[] = array( 'cant-move-to-category-page' ); - } - } elseif ( !$user->isAllowed( $action ) ) { - $errors[] = $this->missingPermissionError( $action, $short ); - } - - return $errors; - } - - /** - * Add the resulting error code to the errors array - * - * @param array $errors List of current errors - * @param array $result Result of errors - * - * @return array List of errors - */ - private function resultToError( $errors, $result ) { - if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { - // A single array representing an error - $errors[] = $result; - } elseif ( is_array( $result ) && is_array( $result[0] ) ) { - // A nested array representing multiple errors - $errors = array_merge( $errors, $result ); - } elseif ( $result !== '' && is_string( $result ) ) { - // A string representing a message-id - $errors[] = array( $result ); - } elseif ( $result === false ) { - // a generic "We don't want them to do that" - $errors[] = array( 'badaccess-group0' ); - } - return $errors; - } - - /** - * Check various permission hooks - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { - // Use getUserPermissionsErrors instead - $result = ''; - if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { - return $result ? array() : array( array( 'badaccess-group0' ) ); - } - // Check getUserPermissionsErrors hook - if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { - $errors = $this->resultToError( $errors, $result ); - } - // Check getUserPermissionsErrorsExpensive hook - if ( - $doExpensiveQueries - && !( $short && count( $errors ) > 0 ) - && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) - ) { - $errors = $this->resultToError( $errors, $result ); - } - - return $errors; - } - - /** - * Check permissions on special pages & namespaces - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkSpecialsAndNSPermissions( $action, $user, $errors, - $doExpensiveQueries, $short - ) { - # Only 'createaccount' can be performed on special pages, - # which don't actually exist in the DB. - if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) { - $errors[] = array( 'ns-specialprotected' ); - } - - # Check $wgNamespaceProtection for restricted namespaces - if ( $this->isNamespaceProtected( $user ) ) { - $ns = $this->mNamespace == NS_MAIN ? - wfMessage( 'nstab-main' )->text() : $this->getNsText(); - $errors[] = $this->mNamespace == NS_MEDIAWIKI ? - array( 'protectedinterface', $action ) : array( 'namespaceprotected', $ns, $action ); - } - - return $errors; - } - - /** - * Check CSS/JS sub-page permissions - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { - # Protect css/js subpages of user pages - # XXX: this might be better using restrictions - # XXX: right 'editusercssjs' is deprecated, for backward compatibility only - if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) { - if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { - if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { - $errors[] = array( 'mycustomcssprotected', $action ); - } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) { - $errors[] = array( 'mycustomjsprotected', $action ); - } - } else { - if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { - $errors[] = array( 'customcssprotected', $action ); - } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { - $errors[] = array( 'customjsprotected', $action ); - } - } - } - - return $errors; - } - - /** - * Check against page_restrictions table requirements on this - * page. The user must possess all required rights for this - * action. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { - foreach ( $this->getRestrictions( $action ) as $right ) { - // Backwards compatibility, rewrite sysop -> editprotected - if ( $right == 'sysop' ) { - $right = 'editprotected'; - } - // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected - if ( $right == 'autoconfirmed' ) { - $right = 'editsemiprotected'; - } - if ( $right == '' ) { - continue; - } - if ( !$user->isAllowed( $right ) ) { - $errors[] = array( 'protectedpagetext', $right, $action ); - } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) { - $errors[] = array( 'protectedpagetext', 'protect', $action ); - } - } - - return $errors; - } - - /** - * Check restrictions on cascading pages. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkCascadingSourcesRestrictions( $action, $user, $errors, - $doExpensiveQueries, $short - ) { - if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { - # We /could/ use the protection level on the source page, but it's - # fairly ugly as we have to establish a precedence hierarchy for pages - # included by multiple cascade-protected pages. So just restrict - # it to people with 'protect' permission, as they could remove the - # protection anyway. - list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); - # Cascading protection depends on more than this page... - # Several cascading protected pages may include this page... - # Check each cascading level - # This is only for protection restrictions, not for all actions - if ( isset( $restrictions[$action] ) ) { - foreach ( $restrictions[$action] as $right ) { - // Backwards compatibility, rewrite sysop -> editprotected - if ( $right == 'sysop' ) { - $right = 'editprotected'; - } - // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected - if ( $right == 'autoconfirmed' ) { - $right = 'editsemiprotected'; - } - if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { - $pages = ''; - foreach ( $cascadingSources as $page ) { - $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; - } - $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages, $action ); - } - } - } - } - - return $errors; - } - - /** - * Check action permissions not already checked in checkQuickPermissions - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkActionPermissions( $action, $user, $errors, - $doExpensiveQueries, $short - ) { - global $wgDeleteRevisionsLimit, $wgLang; - - if ( $action == 'protect' ) { - if ( count( $this->getUserPermissionsErrorsInternal( 'edit', - $user, $doExpensiveQueries, true ) ) - ) { - // If they can't edit, they shouldn't protect. - $errors[] = array( 'protect-cantedit' ); - } - } elseif ( $action == 'create' ) { - $title_protection = $this->getTitleProtection(); - if ( $title_protection ) { - if ( $title_protection['pt_create_perm'] == 'sysop' ) { - $title_protection['pt_create_perm'] = 'editprotected'; // B/C - } - if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) { - $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C - } - if ( $title_protection['pt_create_perm'] == '' - || !$user->isAllowed( $title_protection['pt_create_perm'] ) - ) { - $errors[] = array( - 'titleprotected', - User::whoIs( $title_protection['pt_user'] ), - $title_protection['pt_reason'] - ); - } - } - } elseif ( $action == 'move' ) { - // Check for immobile pages - if ( !MWNamespace::isMovable( $this->mNamespace ) ) { - // Specific message for this case - $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); - } elseif ( !$this->isMovable() ) { - // Less specific message for rarer cases - $errors[] = array( 'immobile-source-page' ); - } - } elseif ( $action == 'move-target' ) { - if ( !MWNamespace::isMovable( $this->mNamespace ) ) { - $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); - } elseif ( !$this->isMovable() ) { - $errors[] = array( 'immobile-target-page' ); - } - } elseif ( $action == 'delete' ) { - $tempErrors = $this->checkPageRestrictions( 'edit', - $user, array(), $doExpensiveQueries, true ); - if ( !$tempErrors ) { - $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit', - $user, $tempErrors, $doExpensiveQueries, true ); - } - if ( $tempErrors ) { - // If protection keeps them from editing, they shouldn't be able to delete. - $errors[] = array( 'deleteprotected' ); - } - if ( $doExpensiveQueries && $wgDeleteRevisionsLimit - && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() - ) { - $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); - } - } - return $errors; - } - - /** - * Check that the user isn't blocked from editing. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { - // Account creation blocks handled at userlogin. - // Unblocking handled in SpecialUnblock - if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { - return $errors; - } - - global $wgEmailConfirmToEdit; - - if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { - $errors[] = array( 'confirmedittext' ); - } - - if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) { - // Don't block the user from editing their own talk page unless they've been - // explicitly blocked from that too. - } elseif ( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { - // @todo FIXME: Pass the relevant context into this function. - $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() ); - } - - return $errors; - } - - /** - * Check that the user is allowed to read this page. - * - * @param string $action The action to check - * @param User $user User to check - * @param array $errors List of current errors - * @param bool $doExpensiveQueries Whether or not to perform expensive queries - * @param bool $short Short circuit on first error - * - * @return array List of errors - */ - private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { - global $wgWhitelistRead, $wgWhitelistReadRegexp; - - $whitelisted = false; - if ( User::isEveryoneAllowed( 'read' ) ) { - # Shortcut for public wikis, allows skipping quite a bit of code - $whitelisted = true; - } elseif ( $user->isAllowed( 'read' ) ) { - # If the user is allowed to read pages, he is allowed to read all pages - $whitelisted = true; - } elseif ( $this->isSpecial( 'Userlogin' ) - || $this->isSpecial( 'ChangePassword' ) - || $this->isSpecial( 'PasswordReset' ) - ) { - # Always grant access to the login page. - # Even anons need to be able to log in. - $whitelisted = true; - } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { - # Time to check the whitelist - # Only do these checks is there's something to check against - $name = $this->getPrefixedText(); - $dbName = $this->getPrefixedDBkey(); - - // Check for explicit whitelisting with and without underscores - if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { - $whitelisted = true; - } elseif ( $this->getNamespace() == NS_MAIN ) { - # Old settings might have the title prefixed with - # a colon for main-namespace pages - if ( in_array( ':' . $name, $wgWhitelistRead ) ) { - $whitelisted = true; - } - } elseif ( $this->isSpecialPage() ) { - # If it's a special page, ditch the subpage bit and check again - $name = $this->getDBkey(); - list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); - if ( $name ) { - $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); - if ( in_array( $pure, $wgWhitelistRead, true ) ) { - $whitelisted = true; - } - } - } - } - - if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { - $name = $this->getPrefixedText(); - // Check for regex whitelisting - foreach ( $wgWhitelistReadRegexp as $listItem ) { - if ( preg_match( $listItem, $name ) ) { - $whitelisted = true; - break; - } - } - } - - if ( !$whitelisted ) { - # If the title is not whitelisted, give extensions a chance to do so... - wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); - if ( !$whitelisted ) { - $errors[] = $this->missingPermissionError( $action, $short ); - } - } - - return $errors; - } - - /** - * Get a description array when the user doesn't have the right to perform - * $action (i.e. when User::isAllowed() returns false) - * - * @param string $action The action to check - * @param bool $short Short circuit on first error - * @return array List of errors - */ - private function missingPermissionError( $action, $short ) { - // We avoid expensive display logic for quickUserCan's and such - if ( $short ) { - return array( 'badaccess-group0' ); - } - - $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), - User::getGroupsWithPermission( $action ) ); - - if ( count( $groups ) ) { - global $wgLang; - return array( - 'badaccess-groups', - $wgLang->commaList( $groups ), - count( $groups ) - ); - } else { - return array( 'badaccess-group0' ); - } - } - - /** - * Can $user perform $action on this page? This is an internal function, - * which checks ONLY that previously checked by userCan (i.e. it leaves out - * checks on wfReadOnly() and blocks) - * - * @param string $action Action that permission needs to be checked for - * @param User $user User to check - * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. - * @param bool $short Set this to true to stop after the first permission error. - * @return array Array of arrays of the arguments to wfMessage to explain permissions problems. - */ - protected function getUserPermissionsErrorsInternal( $action, $user, - $doExpensiveQueries = true, $short = false - ) { - wfProfileIn( __METHOD__ ); - - # Read has special handling - if ( $action == 'read' ) { - $checks = array( - 'checkPermissionHooks', - 'checkReadPermissions', - ); - # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions - # here as it will lead to duplicate error messages. This is okay to do - # since anywhere that checks for create will also check for edit, and - # those checks are called for edit. - } elseif ( $action == 'create' ) { - $checks = array( - 'checkQuickPermissions', - 'checkPermissionHooks', - 'checkPageRestrictions', - 'checkCascadingSourcesRestrictions', - 'checkActionPermissions', - 'checkUserBlock' - ); - } else { - $checks = array( - 'checkQuickPermissions', - 'checkPermissionHooks', - 'checkSpecialsAndNSPermissions', - 'checkCSSandJSPermissions', - 'checkPageRestrictions', - 'checkCascadingSourcesRestrictions', - 'checkActionPermissions', - 'checkUserBlock' - ); - } - - $errors = array(); - while ( count( $checks ) > 0 && - !( $short && count( $errors ) > 0 ) ) { - $method = array_shift( $checks ); - $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); - } - - wfProfileOut( __METHOD__ ); - return $errors; + return $this->getTitleSecurity()->getUserPermissionsErrors( + $action, $user, $doExpensiveQueries, $ignoreErrors + ); } /** @@ -2491,16 +1891,7 @@ * @return array */ public static function getFilteredRestrictionTypes( $exists = true ) { - global $wgRestrictionTypes; - $types = $wgRestrictionTypes; - if ( $exists ) { - # Remove the create restriction for existing titles - $types = array_diff( $types, array( 'create' ) ); - } else { - # Only the create and upload restrictions apply to non-existing titles - $types = array_intersect( $types, array( 'create', 'upload' ) ); - } - return $types; + return $this->getTitleSecurity()->getFilteredRestrictionTypes( $exists ); } /** @@ -2509,70 +1900,14 @@ * @return array Applicable restriction types */ public function getRestrictionTypes() { - if ( $this->isSpecialPage() ) { - return array(); - } - - $types = self::getFilteredRestrictionTypes( $this->exists() ); - - if ( $this->getNamespace() != NS_FILE ) { - # Remove the upload restriction for non-file titles - $types = array_diff( $types, array( 'upload' ) ); - } - - wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); - - wfDebug( __METHOD__ . ': applicable restrictions to [[' . - $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); - - return $types; - } - - /** - * Is this title subject to title protection? - * Title protection is the one applied against creation of such title. - * - * @return array|bool An associative array representing any existent title - * protection, or false if there's none. - */ - private function getTitleProtection() { - // Can't protect pages in special namespaces - if ( $this->getNamespace() < 0 ) { - return false; - } - - // Can't protect pages that exist. - if ( $this->exists() ) { - return false; - } - - if ( $this->mTitleProtection === null ) { - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( - 'protected_titles', - array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ), - array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), - __METHOD__ - ); - - // fetchRow returns false if there are no rows. - $this->mTitleProtection = $dbr->fetchRow( $res ); - } - return $this->mTitleProtection; + return $this->getTitleSecurity()->getRestrictionTypes( $this ); } /** * Remove any title protection due to page existing */ public function deleteTitleProtection() { - $dbw = wfGetDB( DB_MASTER ); - - $dbw->delete( - 'protected_titles', - array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), - __METHOD__ - ); - $this->mTitleProtection = false; + $this->getTitleSecurity()->deleteTitleProtection( $this ); } /** @@ -2583,24 +1918,7 @@ * @return bool */ public function isSemiProtected( $action = 'edit' ) { - global $wgSemiprotectedRestrictionLevels; - - $restrictions = $this->getRestrictions( $action ); - $semi = $wgSemiprotectedRestrictionLevels; - if ( !$restrictions || !$semi ) { - // Not protected, or all protection is full protection - return false; - } - - // Remap autoconfirmed to editsemiprotected for BC - foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) { - $semi[$key] = 'editsemiprotected'; - } - foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) { - $restrictions[$key] = 'editsemiprotected'; - } - - return !array_diff( $restrictions, $semi ); + return $this->getTitleSecurity()->isSemiProtected( $this, $action ); } /** @@ -2611,28 +1929,7 @@ * @return bool */ public function isProtected( $action = '' ) { - global $wgRestrictionLevels; - - $restrictionTypes = $this->getRestrictionTypes(); - - # Special pages have inherent protection - if ( $this->isSpecialPage() ) { - return true; - } - - # Check regular protection levels - foreach ( $restrictionTypes as $type ) { - if ( $action == $type || $action == '' ) { - $r = $this->getRestrictions( $type ); - foreach ( $wgRestrictionLevels as $level ) { - if ( in_array( $level, $r ) && $level != '' ) { - return true; - } - } - } - } - - return false; + return $this->getTitleSecurity()->isProtected( $this, $action ); } /** @@ -2643,16 +1940,7 @@ * @return bool */ public function isNamespaceProtected( User $user ) { - global $wgNamespaceProtection; - - if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { - foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { - if ( $right != '' && !$user->isAllowed( $right ) ) { - return true; - } - } - } - return false; + return $this->getTitleSecurity()->isNamespaceProtected( $this, $user ); } /** @@ -2661,8 +1949,7 @@ * @return bool If the page is subject to cascading restrictions. */ public function isCascadeProtected() { - list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); - return ( $sources > 0 ); + return $this->getTitleSecurity()->isCascadeProtected( $this ); } /** @@ -2675,7 +1962,7 @@ * @since 1.23 */ public function areCascadeProtectionSourcesLoaded( $getPages = true ) { - return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null; + return $this->getTitleSecurity()->areCascadeProtectionSourcesLoaded( $this, $getPages ); } /** @@ -2692,93 +1979,7 @@ * false. */ public function getCascadeProtectionSources( $getPages = true ) { - global $wgContLang; - $pagerestrictions = array(); - - if ( $this->mCascadeSources !== null && $getPages ) { - return array( $this->mCascadeSources, $this->mCascadingRestrictions ); - } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) { - return array( $this->mHasCascadingRestrictions, $pagerestrictions ); - } - - wfProfileIn( __METHOD__ ); - - $dbr = wfGetDB( DB_SLAVE ); - - if ( $this->getNamespace() == NS_FILE ) { - $tables = array( 'imagelinks', 'page_restrictions' ); - $where_clauses = array( - 'il_to' => $this->getDBkey(), - 'il_from=pr_page', - 'pr_cascade' => 1 - ); - } else { - $tables = array( 'templatelinks', 'page_restrictions' ); - $where_clauses = array( - 'tl_namespace' => $this->getNamespace(), - 'tl_title' => $this->getDBkey(), - 'tl_from=pr_page', - 'pr_cascade' => 1 - ); - } - - if ( $getPages ) { - $cols = array( 'pr_page', 'page_namespace', 'page_title', - 'pr_expiry', 'pr_type', 'pr_level' ); - $where_clauses[] = 'page_id=pr_page'; - $tables[] = 'page'; - } else { - $cols = array( 'pr_expiry' ); - } - - $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); - - $sources = $getPages ? array() : false; - $now = wfTimestampNow(); - $purgeExpired = false; - - foreach ( $res as $row ) { - $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); - if ( $expiry > $now ) { - if ( $getPages ) { - $page_id = $row->pr_page; - $page_ns = $row->page_namespace; - $page_title = $row->page_title; - $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); - # Add groups needed for each restriction type if its not already there - # Make sure this restriction type still exists - - if ( !isset( $pagerestrictions[$row->pr_type] ) ) { - $pagerestrictions[$row->pr_type] = array(); - } - - if ( - isset( $pagerestrictions[$row->pr_type] ) - && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) - ) { - $pagerestrictions[$row->pr_type][] = $row->pr_level; - } - } else { - $sources = true; - } - } else { - // Trigger lazy purge of expired restrictions from the db - $purgeExpired = true; - } - } - if ( $purgeExpired ) { - Title::purgeExpiredRestrictions(); - } - - if ( $getPages ) { - $this->mCascadeSources = $sources; - $this->mCascadingRestrictions = $pagerestrictions; - } else { - $this->mHasCascadingRestrictions = $sources; - } - - wfProfileOut( __METHOD__ ); - return array( $sources, $pagerestrictions ); + return $this->getTitleSecurity()->getCascadeProtectionSources( $this, $getPages ); } /** @@ -2789,7 +1990,7 @@ * @since 1.23 */ public function areRestrictionsLoaded() { - return $this->mRestrictionsLoaded; + return $this->getTitleSecurity()->newFromTitle( $this )->areRestrictionsLoaded( $this ); } /** @@ -2800,12 +2001,7 @@ * are required. */ public function getRestrictions( $action ) { - if ( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); - } - return isset( $this->mRestrictions[$action] ) - ? $this->mRestrictions[$action] - : array(); + return $this->getTitleSecurity()->newFromTitle( $this )->getRestrictions( $this, $action ); } /** @@ -2816,10 +2012,7 @@ * @since 1.23 */ public function getAllRestrictions() { - if ( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); - } - return $this->mRestrictions; + return $this->getTitleSecurity()->newFromTitle( $this )->getAllRestrictions( $this ); } /** @@ -2830,10 +2023,7 @@ * or not protected at all, or false if the action is not recognised. */ public function getRestrictionExpiry( $action ) { - if ( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); - } - return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; + return $this->getTitleSecurity()->getRestrictionExpiry( $this, $action ); } /** @@ -2842,28 +2032,7 @@ * @return bool */ function areRestrictionsCascading() { - if ( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); - } - - return $this->mCascadeRestriction; - } - - /** - * Loads a string into mRestrictions array - * - * @param ResultWrapper $res Resource restrictions as an SQL result. - * @param string $oldFashionedRestrictions Comma-separated list of page - * restrictions from page table (pre 1.10) - */ - private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { - $rows = array(); - - foreach ( $res as $row ) { - $rows[] = $row; - } - - $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); + return $this->getTitleSecurity()->areRestrictionsCascading( $this ); } /** @@ -2876,80 +2045,7 @@ * restrictions from page table (pre 1.10) */ public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { - global $wgContLang; - $dbr = wfGetDB( DB_SLAVE ); - - $restrictionTypes = $this->getRestrictionTypes(); - - foreach ( $restrictionTypes as $type ) { - $this->mRestrictions[$type] = array(); - $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); - } - - $this->mCascadeRestriction = false; - - # Backwards-compatibility: also load the restrictions from the page record (old format). - - if ( $oldFashionedRestrictions === null ) { - $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', - array( 'page_id' => $this->getArticleID() ), __METHOD__ ); - } - - if ( $oldFashionedRestrictions != '' ) { - - foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { - $temp = explode( '=', trim( $restrict ) ); - if ( count( $temp ) == 1 ) { - // old old format should be treated as edit/move restriction - $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); - $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); - } else { - $restriction = trim( $temp[1] ); - if ( $restriction != '' ) { //some old entries are empty - $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); - } - } - } - - $this->mOldRestrictions = true; - - } - - if ( count( $rows ) ) { - # Current system - load second to make them override. - $now = wfTimestampNow(); - $purgeExpired = false; - - # Cycle through all the restrictions. - foreach ( $rows as $row ) { - - // Don't take care of restrictions types that aren't allowed - if ( !in_array( $row->pr_type, $restrictionTypes ) ) { - continue; - } - - // This code should be refactored, now that it's being used more generally, - // But I don't really see any harm in leaving it in Block for now -werdna - $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); - - // Only apply the restrictions if they haven't expired! - if ( !$expiry || $expiry > $now ) { - $this->mRestrictionsExpiry[$row->pr_type] = $expiry; - $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); - - $this->mCascadeRestriction |= $row->pr_cascade; - } else { - // Trigger a lazy purge of expired restrictions - $purgeExpired = true; - } - } - - if ( $purgeExpired ) { - Title::purgeExpiredRestrictions(); - } - } - - $this->mRestrictionsLoaded = true; + $this->getTitleSecurity()->loadRestrictionsFromRows( $this, $rows, $oldFashionedRestrictions ); } /** @@ -2959,40 +2055,7 @@ * restrictions from page table (pre 1.10) */ public function loadRestrictions( $oldFashionedRestrictions = null ) { - global $wgContLang; - if ( !$this->mRestrictionsLoaded ) { - if ( $this->exists() ) { - $dbr = wfGetDB( DB_SLAVE ); - - $res = $dbr->select( - 'page_restrictions', - array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ), - array( 'pr_page' => $this->getArticleID() ), - __METHOD__ - ); - - $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); - } else { - $title_protection = $this->getTitleProtection(); - - if ( $title_protection ) { - $now = wfTimestampNow(); - $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); - - if ( !$expiry || $expiry > $now ) { - // Apply the restrictions - $this->mRestrictionsExpiry['create'] = $expiry; - $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); - } else { // Get rid of the old restrictions - Title::purgeExpiredRestrictions(); - $this->mTitleProtection = false; - } - } else { - $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); - } - $this->mRestrictionsLoaded = true; - } - } + $this->getTitleSecurity()->loadRestrictions( $this, $oldFashionedRestrictions ); } /** @@ -3000,32 +2063,14 @@ * This is used when updating protection from WikiPage::doUpdateRestrictions(). */ public function flushRestrictions() { - $this->mRestrictionsLoaded = false; - $this->mTitleProtection = null; + $this->getTitleSecurity()->flushRestrictions( $this ); } /** * Purge expired restrictions from the page_restrictions table */ - static function purgeExpiredRestrictions() { - if ( wfReadOnly() ) { - return; - } - - $method = __METHOD__; - $dbw = wfGetDB( DB_MASTER ); - $dbw->onTransactionIdle( function () use ( $dbw, $method ) { - $dbw->delete( - 'page_restrictions', - array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), - $method - ); - $dbw->delete( - 'protected_titles', - array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), - $method - ); - } ); + public static function purgeExpiredRestrictions() { + $this->getTitleSecurity()->purgeExpiredRestrictions(); } /** @@ -3271,8 +2316,6 @@ } else { $this->mArticleID = intval( $newid ); } - $this->mRestrictionsLoaded = false; - $this->mRestrictions = array(); $this->mRedirect = null; $this->mLength = -1; $this->mLatestID = false; @@ -4753,4 +3796,16 @@ wfRunHooks( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) ); return $notices; } + + /** + * Get the protection helper for this Title + * + * @return TitleSecurity + */ + protected function getTitleSecurity() { + if ( !$this->mTitleSecurity ) { + $this->mTitleSecurity = TitleSecurity::newFromTitle( $this ); + } + return $this->mTitleSecurity; + } } diff --git a/includes/security/TitleSecurity.php b/includes/security/TitleSecurity.php new file mode 100644 index 0000000..b2d190c --- /dev/null +++ b/includes/security/TitleSecurity.php @@ -0,0 +1,1217 @@ +<?php + +/** + * Encapsulates Title-based security logic + * + * @since 1.24 + * + * TODO: resolve lazy-loading mess + */ +class TitleSecurity { + /** @var Title title to be protected */ + protected $title; + + /** @var array Array of groups allowed to edit this article */ + protected $mRestrictions = array(); + + /** @var bool */ + protected $mOldRestrictions = false; + + /** @var bool Cascade restrictions on this page to included templates and images? */ + protected $mCascadeRestriction; + + /** Caching the results of getCascadeProtectionSources */ + protected $mCascadingRestrictions; + + /** @var array When do the restrictions on this page expire? */ + protected $mRestrictionsExpiry = array(); + + /** @var bool Are cascading restrictions in effect on this page? */ + protected $mHasCascadingRestrictions; + + /** @var array Where are the cascading restrictions coming from on this page? */ + protected $mCascadeSources; + + /** @var bool Boolean for initialisation on demand */ + protected $mRestrictionsLoaded = false; + + /** @var mixed Cached value for getTitleProtection (create protection) */ + protected $mTitleProtection; + + public static function newFromTitle( Title $title ) { + $p = new TitleSecurity(); + $p->title = $title; + return $p; + } + + /** + * Can $user perform $action on this page? + * This skips potentially expensive cascading permission checks + * as well as avoids expensive error formatting + * + * Suitable for use for nonessential UI controls in common cases, but + * _not_ for functional access control. + * + * May provide false positives, but should never provide a false negative. + * + * @param string $action Action that permission needs to be checked for + * @param User $user User to check; $wgUser will be used if not provided. + * @return bool + */ + public function quickUserCan( $action, $user = null ) { + return $this->userCan( $action, $user, false ); + } + + /** + * Can $user perform $action on this page? + * + * @param string $action Action that permission needs to be checked for + * @param User $user User to check; $wgUser will be used if not + * provided. + * @param bool $doExpensiveQueries Set this to false to avoid doing + * unnecessary queries. + * @return bool + */ + public function userCan( $action, $user = null, $doExpensiveQueries = true ) { + if ( !$user instanceof User ) { + global $wgUser; + $user = $wgUser; + } + + return !count( self::getUserPermissionsErrorsInternals( + $action, $user, $doExpensiveQueries, true ) ); + } + + /** + * Can $user perform $action on this page? + * + * @todo FIXME: This *does not* check throttles (User::pingLimiter()). + * + * @param string $action Action that permission needs to be checked for + * @param User $user User to check + * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary + * queries by skipping checks for cascading protections and user blocks. + * @param array $ignoreErrors Array of Strings Set this to a list of message keys + * whose corresponding errors may be ignored. + * @return array Array of arguments to wfMessage to explain permissions problems. + */ + public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, + $ignoreErrors = array() + ) { + $errors = self::getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); + + // Remove the errors being ignored. + foreach ( $errors as $index => $error ) { + $error_key = is_array( $error ) ? $error[0] : $error; + + if ( in_array( $error_key, $ignoreErrors ) ) { + unset( $errors[$index] ); + } + } + + return $errors; + } + + /** + * Permissions checks that fail most often, and which are easiest to test. + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkQuickPermissions( $action, $user, $errors, + $doExpensiveQueries, $short + ) { + if ( !wfRunHooks( 'TitleQuickPermissions', + array( $this->title, $user, $action, &$errors, $doExpensiveQueries, $short ) ) + ) { + return $errors; + } + + if ( $action == 'create' ) { + if ( + ( $this->title->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || + ( !$this->title->isTalkPage() && !$user->isAllowed( 'createpage' ) ) + ) { + $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); + } + } elseif ( $action == 'move' ) { + if ( !$user->isAllowed( 'move-rootuserpages' ) + && $this->title->getNamespace() == NS_USER && !$this->title->isSubpage() ) { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-user-page' ); + } + + // Check if user is allowed to move files if it's a file + if ( $this->title->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { + $errors[] = array( 'movenotallowedfile' ); + } + + // Check if user is allowed to move category pages if it's a category page + if ( $this->title->getNamespace() == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) { + $errors[] = array( 'cant-move-category-page' ); + } + + if ( !$user->isAllowed( 'move' ) ) { + // User can't move anything + $userCanMove = User::groupHasPermission( 'user', 'move' ); + $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); + if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { + // custom message if logged-in users without any special rights can move + $errors[] = array( 'movenologintext' ); + } else { + $errors[] = array( 'movenotallowed' ); + } + } + } elseif ( $action == 'move-target' ) { + if ( !$user->isAllowed( 'move' ) ) { + // User can't move anything + $errors[] = array( 'movenotallowed' ); + } elseif ( !$user->isAllowed( 'move-rootuserpages' ) + && $this->title->getNamespace() == NS_USER && !$this->title->isSubpage() ) { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-to-user-page' ); + } elseif ( !$user->isAllowed( 'move-categorypages' ) + && $this->title->getNamespace() == NS_CATEGORY ) { + // Show category page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-to-category-page' ); + } + } elseif ( !$user->isAllowed( $action ) ) { + $errors[] = self::missingPermissionError( $action, $short ); + } + + return $errors; + } + + /** + * Add the resulting error code to the errors array + * + * @param array $errors List of current errors + * @param array $result Result of errors + * + * @return array List of errors + */ + protected function resultToError( $errors, $result ) { + if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { + // A single array representing an error + $errors[] = $result; + } elseif ( is_array( $result ) && is_array( $result[0] ) ) { + // A nested array representing multiple errors + $errors = array_merge( $errors, $result ); + } elseif ( $result !== '' && is_string( $result ) ) { + // A string representing a message-id + $errors[] = array( $result ); + } elseif ( $result === false ) { + // a generic "We don't want them to do that" + $errors[] = array( 'badaccess-group0' ); + } + return $errors; + } + + /** + * Check various permission hooks + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { + // Use getUserPermissionsErrors instead + $result = ''; + if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { + return $result ? array() : array( array( 'badaccess-group0' ) ); + } + // Check getUserPermissionsErrors hook + if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { + $errors = self::resultToError( $errors, $result ); + } + // Check getUserPermissionsErrorsExpensive hook + if ( + $doExpensiveQueries + && !( $short && count( $errors ) > 0 ) + && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) + ) { + $errors = self::resultToError( $errors, $result ); + } + + return $errors; + } + + /** + * Check permissions on special pages & namespaces + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkSpecialsAndNSPermissions( $action, $user, $errors, + $doExpensiveQueries, $short + ) { + # Only 'createaccount' can be performed on special pages, + # which don't actually exist in the DB. + if ( NS_SPECIAL == $this->title->getNamespace() && $action !== 'createaccount' ) { + $errors[] = array( 'ns-specialprotected' ); + } + + # Check $wgNamespaceProtection for restricted namespaces + if ( self::isNamespaceProtected( $user ) ) { + $ns = $this->title->getNamespace() == NS_MAIN ? + wfMessage( 'nstab-main' )->text() : $this->title->getNsText(); + $errors[] = $this->title->getNamespace() == NS_MEDIAWIKI ? + array( 'protectedinterface', $action ) : array( 'namespaceprotected', $ns, $action ); + } + + return $errors; + } + + /** + * Check CSS/JS sub-page permissions + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { + # Protect css/js subpages of user pages + # XXX: this might be better using restrictions + # XXX: right 'editusercssjs' is deprecated, for backward compatibility only + if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) { + if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->title->getText() ) ) { + if ( $this->title->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { + $errors[] = array( 'mycustomcssprotected', $action ); + } elseif ( $this->title->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) { + $errors[] = array( 'mycustomjsprotected', $action ); + } + } else { + if ( $this->title->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { + $errors[] = array( 'customcssprotected', $action ); + } elseif ( $this->title->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { + $errors[] = array( 'customjsprotected', $action ); + } + } + } + + return $errors; + } + + /** + * Check against page_restrictions table requirements on this + * page. The user must possess all required rights for this + * action. + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { + foreach ( $this->getRestrictions( $action ) as $right ) { + // Backwards compatibility, rewrite sysop -> editprotected + if ( $right == 'sysop' ) { + $right = 'editprotected'; + } + // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; + } + if ( $right == '' ) { + continue; + } + if ( !$user->isAllowed( $right ) ) { + $errors[] = array( 'protectedpagetext', $right, $action ); + } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) { + $errors[] = array( 'protectedpagetext', 'protect', $action ); + } + } + + return $errors; + } + + /** + * Check restrictions on cascading pages. + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkCascadingSourcesRestrictions( $action, $user, $errors, + $doExpensiveQueries, $short + ) { + if ( $doExpensiveQueries && !$this->title->isCssJsSubpage() ) { + # We /could/ use the protection level on the source page, but it's + # fairly ugly as we have to establish a precedence hierarchy for pages + # included by multiple cascade-protected pages. So just restrict + # it to people with 'protect' permission, as they could remove the + # protection anyway. + list( $cascadingSources, $restrictions ) = self::getCascadeProtectionSources(); + # Cascading protection depends on more than this page... + # Several cascading protected pages may include this page... + # Check each cascading level + # This is only for protection restrictions, not for all actions + if ( isset( $restrictions[$action] ) ) { + foreach ( $restrictions[$action] as $right ) { + // Backwards compatibility, rewrite sysop -> editprotected + if ( $right == 'sysop' ) { + $right = 'editprotected'; + } + // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected + if ( $right == 'autoconfirmed' ) { + $right = 'editsemiprotected'; + } + if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { + $pages = ''; + foreach ( $cascadingSources as $page ) { + $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; + } + $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages, $action ); + } + } + } + } + + return $errors; + } + + /** + * Check action permissions not already checked in checkQuickPermissions + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkActionPermissions( $action, $user, $errors, + $doExpensiveQueries, $short + ) { + global $wgDeleteRevisionsLimit, $wgLang; + + if ( $action == 'protect' ) { + if ( count( self::getUserPermissionsErrorsInternal( 'edit', + $user, $doExpensiveQueries, true ) ) + ) { + // If they can't edit, they shouldn't protect. + $errors[] = array( 'protect-cantedit' ); + } + } elseif ( $action == 'create' ) { + $title_protection = self::getTitleProtection(); + if ( $title_protection ) { + if ( $title_protection['pt_create_perm'] == 'sysop' ) { + $title_protection['pt_create_perm'] = 'editprotected'; // B/C + } + if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) { + $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C + } + if ( $title_protection['pt_create_perm'] == '' + || !$user->isAllowed( $title_protection['pt_create_perm'] ) + ) { + $errors[] = array( + 'titleprotected', + User::whoIs( $title_protection['pt_user'] ), + $title_protection['pt_reason'] + ); + } + } + } elseif ( $action == 'move' ) { + // Check for immobile pages + if ( !MWNamespace::isMovable( $this->title->getNamespace() ) ) { + // Specific message for this case + $errors[] = array( 'immobile-source-namespace', $this->title->getNsText() ); + } elseif ( !$this->title->isMovable() ) { + // Less specific message for rarer cases + $errors[] = array( 'immobile-source-page' ); + } + } elseif ( $action == 'move-target' ) { + if ( !MWNamespace::isMovable( $this->title->getNamespace() ) ) { + $errors[] = array( 'immobile-target-namespace', $this->title->getNsText() ); + } elseif ( !$this->title->isMovable() ) { + $errors[] = array( 'immobile-target-page' ); + } + } elseif ( $action == 'delete' ) { + $tempErrors = self::checkPageRestrictions( 'edit', + $user, array(), $doExpensiveQueries, true ); + if ( !$tempErrors ) { + $tempErrors = self::checkCascadingSourcesRestrictions( 'edit', + $user, $tempErrors, $doExpensiveQueries, true ); + } + if ( $tempErrors ) { + // If protection keeps them from editing, they shouldn't be able to delete. + $errors[] = array( 'deleteprotected' ); + } + if ( $doExpensiveQueries && $wgDeleteRevisionsLimit + && !self::userCan( 'bigdelete', $user ) && $this->title->isBigDeletion() + ) { + $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); + } + } + return $errors; + } + + /** + * Check that the user isn't blocked from editing. + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { + // Account creation blocks handled at userlogin. + // Unblocking handled in SpecialUnblock + if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { + return $errors; + } + + global $wgEmailConfirmToEdit; + + if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { + $errors[] = array( 'confirmedittext' ); + } + + if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this->title ) ) { + // Don't block the user from editing their own talk page unless they've been + // explicitly blocked from that too. + } elseif ( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { + // @todo FIXME: Pass the relevant context into this function. + $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() ); + } + + return $errors; + } + + /** + * Check that the user is allowed to read this page. + * + * @param string $action The action to check + * @param User $user User to check + * @param array $errors List of current errors + * @param bool $doExpensiveQueries Whether or not to perform expensive queries + * @param bool $short Short circuit on first error + * + * @return array List of errors + */ + protected function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { + global $wgWhitelistRead, $wgWhitelistReadRegexp; + + $whitelisted = false; + if ( User::isEveryoneAllowed( 'read' ) ) { + # Shortcut for public wikis, allows skipping quite a bit of code + $whitelisted = true; + } elseif ( $user->isAllowed( 'read' ) ) { + # If the user is allowed to read pages, he is allowed to read all pages + $whitelisted = true; + } elseif ( $this->title->isSpecial( 'Userlogin' ) + || $this->title->isSpecial( 'ChangePassword' ) + || $this->title->isSpecial( 'PasswordReset' ) + ) { + # Always grant access to the login page. + # Even anons need to be able to log in. + $whitelisted = true; + } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { + # Time to check the whitelist + # Only do these checks is there's something to check against + $name = $this->title->getPrefixedText(); + $dbName = $this->title->getPrefixedDBkey(); + + // Check for explicit whitelisting with and without underscores + if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { + $whitelisted = true; + } elseif ( $this->title->getNamespace() == NS_MAIN ) { + # Old settings might have the title prefixed with + # a colon for main-namespace pages + if ( in_array( ':' . $name, $wgWhitelistRead ) ) { + $whitelisted = true; + } + } elseif ( $this->title->isSpecialPage() ) { + # If it's a special page, ditch the subpage bit and check again + $name = $this->title->getDBkey(); + list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); + if ( $name ) { + $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); + if ( in_array( $pure, $wgWhitelistRead, true ) ) { + $whitelisted = true; + } + } + } + } + + if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { + $name = $this->title->getPrefixedText(); + // Check for regex whitelisting + foreach ( $wgWhitelistReadRegexp as $listItem ) { + if ( preg_match( $listItem, $name ) ) { + $whitelisted = true; + break; + } + } + } + + if ( !$whitelisted ) { + # If the title is not whitelisted, give extensions a chance to do so... + wfRunHooks( 'TitleReadWhitelist', array( $this->title, $user, &$whitelisted ) ); + if ( !$whitelisted ) { + $errors[] = self::missingPermissionError( $action, $short ); + } + } + + return $errors; + } + + /** + * Get a description array when the user doesn't have the right to perform + * $action (i.e. when User::isAllowed() returns false) + * + * @param string $action The action to check + * @param bool $short Short circuit on first error + * @return array List of errors + */ + protected function missingPermissionError( $action, $short ) { + // We avoid expensive display logic for quickUserCan's and such + if ( $short ) { + return array( 'badaccess-group0' ); + } + + $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), + User::getGroupsWithPermission( $action ) ); + + if ( count( $groups ) ) { + global $wgLang; + return array( + 'badaccess-groups', + $wgLang->commaList( $groups ), + count( $groups ) + ); + } else { + return array( 'badaccess-group0' ); + } + } + + /** + * Can $user perform $action on this page? This is an internal function, + * which checks ONLY that previously checked by userCan (i.e. it leaves out + * checks on wfReadOnly() and blocks) + * + * @param string $action Action that permission needs to be checked for + * @param User $user User to check + * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. + * @param bool $short Set this to true to stop after the first permission error. + * @return array Array of arrays of the arguments to wfMessage to explain permissions problems. + */ + protected function getUserPermissionsErrorsInternal( $action, $user, + $doExpensiveQueries = true, $short = false + ) { + wfProfileIn( __METHOD__ ); + + # Read has special handling + if ( $action == 'read' ) { + $checks = array( + 'checkPermissionHooks', + 'checkReadPermissions', + ); + # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions + # here as it will lead to duplicate error messages. This is okay to do + # since anywhere that checks for create will also check for edit, and + # those checks are called for edit. + } elseif ( $action == 'create' ) { + $checks = array( + 'checkQuickPermissions', + 'checkPermissionHooks', + 'checkPageRestrictions', + 'checkCascadingSourcesRestrictions', + 'checkActionPermissions', + 'checkUserBlock' + ); + } else { + $checks = array( + 'checkQuickPermissions', + 'checkPermissionHooks', + 'checkSpecialsAndNSPermissions', + 'checkCSSandJSPermissions', + 'checkPageRestrictions', + 'checkCascadingSourcesRestrictions', + 'checkActionPermissions', + 'checkUserBlock' + ); + } + + $errors = array(); + while ( count( $checks ) > 0 && + !( $short && count( $errors ) > 0 ) ) { + $method = array_shift( $checks ); + $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); + } + + wfProfileOut( __METHOD__ ); + return $errors; + } + + /** + * Get a filtered list of all restriction types supported by this wiki. + * @param bool $exists True to get all restriction types that apply to + * titles that do exist, False for all restriction types that apply to + * titles that do not exist + * @return array + */ + public static function getFilteredRestrictionTypes( $exists = true ) { + global $wgRestrictionTypes; + $types = $wgRestrictionTypes; + if ( $exists ) { + # Remove the create restriction for existing titles + $types = array_diff( $types, array( 'create' ) ); + } else { + # Only the create and upload restrictions apply to non-existing titles + $types = array_intersect( $types, array( 'create', 'upload' ) ); + } + return $types; + } + + /** + * Returns restriction types for the current Title + * + * @return array Applicable restriction types + */ + public function getRestrictionTypes() { + if ( $this->title->isSpecialPage() ) { + return array(); + } + + $types = self::getFilteredRestrictionTypes( $this->title->exists() ); + + if ( $this->title->getNamespace() != NS_FILE ) { + # Remove the upload restriction for non-file titles + $types = array_diff( $types, array( 'upload' ) ); + } + + wfRunHooks( 'TitleGetRestrictionTypes', array( $title, &$types ) ); + + wfDebug( __METHOD__ . ': applicable restrictions to [[' . + $this->title->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); + + return $types; + } + + /** + * Is this title subject to title protection? + * Title protection is the one applied against creation of such title. + * + * @return array|bool An associative array representing any existent title + * protection, or false if there's none. + */ + protected function getTitleProtection() { + // Can't protect pages in special namespaces + if ( $this->title->getNamespace() < 0 ) { + return false; + } + + // Can't protect pages that exist. + if ( $this->title->exists() ) { + return false; + } + + if ( $this->mTitleProtection === null ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( + 'protected_titles', + array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ), + array( 'pt_namespace' => $this->title->getNamespace(), 'pt_title' => $this->title->getDBkey() ), + __METHOD__ + ); + + // fetchRow returns false if there are no rows. + $this->mTitleProtection = $dbr->fetchRow( $res ); + } + return $this->mTitleProtection; + } + + /** + * Remove any title protection due to page existing + */ + protected function deleteTitleProtection() { + $dbw = wfGetDB( DB_MASTER ); + + $dbw->delete( + 'protected_titles', + array( 'pt_namespace' => $this->title->getNamespace(), 'pt_title' => $this->title->getDBkey() ), + __METHOD__ + ); + $this->mTitleProtection = false; + } + + /** + * Is this page "semi-protected" - the *only* protection levels are listed + * in $wgSemiprotectedRestrictionLevels? + * + * @param string $action Action to check (default: edit) + * @return bool + */ + public function isSemiProtected( $action = 'edit' ) { + global $wgSemiprotectedRestrictionLevels; + + $restrictions = $this->getRestrictions( $action ); + $semi = $wgSemiprotectedRestrictionLevels; + if ( !$restrictions || !$semi ) { + // Not protected, or all protection is full protection + return false; + } + + // Remap autoconfirmed to editsemiprotected for BC + foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) { + $semi[$key] = 'editsemiprotected'; + } + foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) { + $restrictions[$key] = 'editsemiprotected'; + } + + return !array_diff( $restrictions, $semi ); + } + + /** + * Does the title correspond to a protected article? + * + * @param string $action The action the page is protected from, + * by default checks all actions. + * @return bool + */ + public function isProtected( $action = '' ) { + global $wgRestrictionLevels; + + $restrictionTypes = $this->getRestrictionTypes(); + + # Special pages have inherent protection + if ( $this->title->isSpecialPage() ) { + return true; + } + + # Check regular protection levels + foreach ( $restrictionTypes as $type ) { + if ( $action == $type || $action == '' ) { + $r = $this->getRestrictions( $type ); + foreach ( $wgRestrictionLevels as $level ) { + if ( in_array( $level, $r ) && $level != '' ) { + return true; + } + } + } + } + + return false; + } + + /** + * Determines if $user is unable to edit this page because it has been protected + * by $wgNamespaceProtection. + * + * @param User $user User object to check permissions + * @return bool + */ + public function isNamespaceProtected( User $user ) { + global $wgNamespaceProtection; + + if ( isset( $wgNamespaceProtection[$this->title->getNamespace()] ) ) { + foreach ( (array)$wgNamespaceProtection[$this->title->getNamespace()] as $right ) { + if ( $right != '' && !$user->isAllowed( $right ) ) { + return true; + } + } + } + return false; + } + + /** + * Cascading protection: Return true if cascading restrictions apply to this page, false if not. + * + * @return bool If the page is subject to cascading restrictions. + */ + public function isCascadeProtected() { + list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); + return ( $sources > 0 ); + } + + /** + * Determines whether cascading protection sources have already been loaded from + * the database. + * + * @param bool $getPages True to check if the pages are loaded, or false to check + * if the status is loaded. + * @return bool Whether or not the specified information has been loaded + */ + public function areCascadeProtectionSourcesLoaded( $getPages = true ) { + return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null; + } + + /** + * Cascading protection: Get the source of any cascading restrictions on this page. + * + * @param bool $getPages Whether or not to retrieve the actual pages + * that the restrictions have come from and the actual restrictions + * themselves. + * @return array Two elements: First is an array of Title objects of the + * pages from which cascading restrictions have come, false for + * none, or true if such restrictions exist but $getPages was not + * set. Second is an array like that returned by + * TitleSecurity::getAllRestrictions(), or an empty array if $getPages is + * false. + */ + public function getCascadeProtectionSources( $getPages = true ) { + global $wgContLang; + $pagerestrictions = array(); + + if ( $this->mCascadeSources !== null && $getPages ) { + return array( $this->mCascadeSources, $this->mCascadingRestrictions ); + } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) { + return array( $this->mHasCascadingRestrictions, $pagerestrictions ); + } + + wfProfileIn( __METHOD__ ); + + $dbr = wfGetDB( DB_SLAVE ); + + if ( $this->title->getNamespace() == NS_FILE ) { + $tables = array( 'imagelinks', 'page_restrictions' ); + $where_clauses = array( + 'il_to' => $this->title->getDBkey(), + 'il_from=pr_page', + 'pr_cascade' => 1 + ); + } else { + $tables = array( 'templatelinks', 'page_restrictions' ); + $where_clauses = array( + 'tl_namespace' => $this->title->getNamespace(), + 'tl_title' => $this->title->getDBkey(), + 'tl_from=pr_page', + 'pr_cascade' => 1 + ); + } + + if ( $getPages ) { + $cols = array( 'pr_page', 'page_namespace', 'page_title', + 'pr_expiry', 'pr_type', 'pr_level' ); + $where_clauses[] = 'page_id=pr_page'; + $tables[] = 'page'; + } else { + $cols = array( 'pr_expiry' ); + } + + $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); + + $sources = $getPages ? array() : false; + $now = wfTimestampNow(); + $purgeExpired = false; + + foreach ( $res as $row ) { + $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); + if ( $expiry > $now ) { + if ( $getPages ) { + $page_id = $row->pr_page; + $page_ns = $row->page_namespace; + $page_title = $row->page_title; + $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); + # Add groups needed for each restriction type if its not already there + # Make sure this restriction type still exists + + if ( !isset( $pagerestrictions[$row->pr_type] ) ) { + $pagerestrictions[$row->pr_type] = array(); + } + + if ( + isset( $pagerestrictions[$row->pr_type] ) + && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) + ) { + $pagerestrictions[$row->pr_type][] = $row->pr_level; + } + } else { + $sources = true; + } + } else { + // Trigger lazy purge of expired restrictions from the db + $purgeExpired = true; + } + } + if ( $purgeExpired ) { + TitleSecurity::purgeExpiredRestrictions(); + } + + if ( $getPages ) { + $this->mCascadeSources = $sources; + $this->mCascadingRestrictions = $pagerestrictions; + } else { + $this->mHasCascadingRestrictions = $sources; + } + + wfProfileOut( __METHOD__ ); + return array( $sources, $pagerestrictions ); + } + + /** + * Accessor for mRestrictionsLoaded + * + * @return bool Whether or not the page's restrictions have already been + * loaded from the database + */ + public function areRestrictionsLoaded() { + return $this->mRestrictionsLoaded; + } + + /** + * Accessor/initialisation for mRestrictions + * + * @param string $action Action that permission needs to be checked for + * @return array Restriction levels needed to take the action. All levels + * are required. + */ + public function getRestrictions( $action ) { + if ( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + return isset( $this->mRestrictions[$action] ) + ? $this->mRestrictions[$action] + : array(); + } + + /** + * Accessor/initialisation for mRestrictions + * + * @return array Keys are actions, values are arrays as returned by + * TitleSecurity::getRestrictions() + */ + public function getAllRestrictions() { + if ( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + return $this->mRestrictions; + } + + /** + * Get the expiry time for the restriction against a given action + * + * @param string $action + * @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever + * or not protected at all, or false if the action is not recognised. + */ + public function getRestrictionExpiry( $action ) { + if ( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; + } + + /** + * Returns cascading restrictions for the current article + * + * @return bool + */ + public function areRestrictionsCascading() { + if ( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + + return $this->mCascadeRestriction; + } + + /** + * Loads a string into mRestrictions array + * + * @param ResultWrapper $res Resource restrictions as an SQL result. + * @param string $oldFashionedRestrictions Comma-separated list of page + * restrictions from page table (pre 1.10) + */ + protected function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { + $rows = array(); + + foreach ( $res as $row ) { + $rows[] = $row; + } + + $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); + } + + /** + * Compiles list of active page restrictions from both page table (pre 1.10) + * and page_restrictions table for this existing page. + * Public for usage by LiquidThreads. + * + * @param array $rows Array of db result objects + * @param string $oldFashionedRestrictions Comma-separated list of page + * restrictions from page table (pre 1.10) + */ + public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { + global $wgContLang; + $dbr = wfGetDB( DB_SLAVE ); + + $restrictionTypes = $this->getRestrictionTypes(); + + foreach ( $restrictionTypes as $type ) { + $this->mRestrictions[$type] = array(); + $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); + } + + $this->mCascadeRestriction = false; + + # Backwards-compatibility: also load the restrictions from the page record (old format). + + if ( $oldFashionedRestrictions === null ) { + $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', + array( 'page_id' => $this->title->getArticleID() ), __METHOD__ ); + } + + if ( $oldFashionedRestrictions != '' ) { + + foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { + $temp = explode( '=', trim( $restrict ) ); + if ( count( $temp ) == 1 ) { + // old old format should be treated as edit/move restriction + $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); + $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); + } else { + $restriction = trim( $temp[1] ); + if ( $restriction != '' ) { //some old entries are empty + $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); + } + } + } + + $this->mOldRestrictions = true; + + } + + if ( count( $rows ) ) { + # Current system - load second to make them override. + $now = wfTimestampNow(); + $purgeExpired = false; + + # Cycle through all the restrictions. + foreach ( $rows as $row ) { + + // Don't take care of restrictions types that aren't allowed + if ( !in_array( $row->pr_type, $restrictionTypes ) ) { + continue; + } + + // This code should be refactored, now that it's being used more generally, + // But I don't really see any harm in leaving it in Block for now -werdna + $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); + + // Only apply the restrictions if they haven't expired! + if ( !$expiry || $expiry > $now ) { + $this->mRestrictionsExpiry[$row->pr_type] = $expiry; + $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); + + $this->mCascadeRestriction |= $row->pr_cascade; + } else { + // Trigger a lazy purge of expired restrictions + $purgeExpired = true; + } + } + + if ( $purgeExpired ) { + TitleSecurity::purgeExpiredRestrictions(); + } + } + + $this->mRestrictionsLoaded = true; + } + + /** + * Load restrictions from the page_restrictions table + * + * @param string $oldFashionedRestrictions Comma-separated list of page + * restrictions from page table (pre 1.10) + */ + public function loadRestrictions( $oldFashionedRestrictions = null ) { + global $wgContLang; + if ( !$this->mRestrictionsLoaded ) { + if ( $this->title->exists() ) { + $dbr = wfGetDB( DB_SLAVE ); + + $res = $dbr->select( + 'page_restrictions', + array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ), + array( 'pr_page' => $this->title->getArticleID() ), + __METHOD__ + ); + + $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); + } else { + $title_protection = $this->getTitleProtection(); + + if ( $title_protection ) { + $now = wfTimestampNow(); + $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); + + if ( !$expiry || $expiry > $now ) { + // Apply the restrictions + $this->mRestrictionsExpiry['create'] = $expiry; + $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); + } else { // Get rid of the old restrictions + TitleSecurity::purgeExpiredRestrictions(); + $this->mTitleProtection = false; + } + } else { + $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); + } + $this->mRestrictionsLoaded = true; + } + } + } + + /** + * Flush the protection cache in this object and force reload from the database. + * This is used when updating protection from WikiPage::doUpdateRestrictions(). + */ + public function flushRestrictions() { + $this->mRestrictionsLoaded = false; + $this->mTitleProtection = null; + } + + /** + * Purge expired restrictions from the page_restrictions table + */ + public static function purgeExpiredRestrictions() { + if ( wfReadOnly() ) { + return; + } + + $method = __METHOD__; + $dbw = wfGetDB( DB_MASTER ); + $dbw->onTransactionIdle( function () use ( $dbw, $method ) { + $dbw->delete( + 'page_restrictions', + array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), + $method + ); + $dbw->delete( + 'protected_titles', + array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), + $method + ); + } ); + } +} -- To view, visit https://gerrit.wikimedia.org/r/166357 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ifa5671f5e4cf16cf5447bf132b5081af95e543ee Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Awight <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
