* @file
*/
+use MediaWiki\Permissions\PermissionManager;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
* and does not rely on global state or the database.
*/
class Title implements LinkTarget, IDBAccessObject {
- /** @var MapCacheLRU */
- static private $titleCache = null;
+ /** @var MapCacheLRU|null */
+ private static $titleCache = null;
/**
* Title::newFromText maintains a cache to avoid expensive re-normalization of
*/
const GAID_FOR_UPDATE = 1;
+ /**
+ * Flag for use with factory methods like newFromLinkTarget() that have
+ * a $forceClone parameter. If set, the method must return a new instance.
+ * Without this flag, some factory methods may return existing instances.
+ *
+ * @since 1.33
+ */
+ const NEW_CLONE = 'clone';
+
/**
* @name Private member variables
* Please use the accessor functions instead.
* Only public to share cache with TitleFormatter
*
* @private
- * @var string
+ * @var string|null
*/
public $prefixedText = null;
* the database or false if not loaded, yet. */
private $mDbPageLanguage = false;
- /** @var TitleValue A corresponding TitleValue object */
+ /** @var TitleValue|null A corresponding TitleValue object */
private $mTitleValue = null;
- /** @var bool Would deleting this page be a big deletion? */
+ /** @var bool|null Would deleting this page be a big deletion? */
private $mIsBigDeletion = null;
// @}
}
/**
- * @access protected
+ * @protected
*/
function __construct() {
}
* @return Title|null Title, or null on an error
*/
public static function newFromDBkey( $key ) {
- $t = new Title();
+ $t = new self();
$t->mDbkeyform = $key;
try {
}
/**
- * Create a new Title from a TitleValue
+ * Returns a Title given a TitleValue.
+ * If the given TitleValue is already a Title instance, that instance is returned,
+ * unless $forceClone is "clone". If $forceClone is "clone" and the given TitleValue
+ * is already a Title instance, that instance is copied using the clone operator.
*
* @param TitleValue $titleValue Assumed to be safe.
+ * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned.
*
* @return Title
*/
- public static function newFromTitleValue( TitleValue $titleValue ) {
- return self::newFromLinkTarget( $titleValue );
+ public static function newFromTitleValue( TitleValue $titleValue, $forceClone = '' ) {
+ return self::newFromLinkTarget( $titleValue, $forceClone );
}
/**
- * Create a new Title from a LinkTarget
+ * Returns a Title given a LinkTarget.
+ * If the given LinkTarget is already a Title instance, that instance is returned,
+ * unless $forceClone is "clone". If $forceClone is "clone" and the given LinkTarget
+ * is already a Title instance, that instance is copied using the clone operator.
*
* @param LinkTarget $linkTarget Assumed to be safe.
+ * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned.
*
* @return Title
*/
- public static function newFromLinkTarget( LinkTarget $linkTarget ) {
+ public static function newFromLinkTarget( LinkTarget $linkTarget, $forceClone = '' ) {
if ( $linkTarget instanceof Title ) {
// Special case if it's already a Title object
- return $linkTarget;
+ if ( $forceClone === self::NEW_CLONE ) {
+ return clone $linkTarget;
+ } else {
+ return $linkTarget;
+ }
}
return self::makeTitle(
$linkTarget->getNamespace(),
* Title objects returned by this method are guaranteed to be valid, and
* thus return true from the isValid() method.
*
+ * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
+ * It may instead be a cached instance created previously, with references to it remaining
+ * elsewhere.
+ *
* @param string|int|null $text The link text; spaces, prefixes, and an
* initial ':' indicating the main namespace are accepted.
* @param int $defaultNamespace The namespace to use if none is specified
}
try {
- return self::newFromTextThrow( strval( $text ), $defaultNamespace );
+ return self::newFromTextThrow( (string)$text, $defaultNamespace );
} catch ( MalformedTitleException $ex ) {
return null;
}
* Title objects returned by this method are guaranteed to be valid, and
* thus return true from the isValid() method.
*
+ * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
+ * It may instead be a cached instance created previously, with references to it remaining
+ * elsewhere.
+ *
* @see Title::newFromText
*
* @since 1.25
$t = new Title();
$t->mDbkeyform = strtr( $filteredText, ' ', '_' );
- $t->mDefaultNamespace = intval( $defaultNamespace );
+ $t->mDefaultNamespace = (int)$defaultNamespace;
$t->secureAndSplit();
if ( $defaultNamespace == NS_MAIN ) {
* @return MapCacheLRU
*/
private static function getTitleCache() {
- if ( self::$titleCache == null ) {
+ if ( self::$titleCache === null ) {
self::$titleCache = new MapCacheLRU( self::CACHE_MAX );
}
return self::$titleCache;
} else {
$title = null;
}
+
return $title;
}
$this->mLatestID = (int)$row->page_latest;
}
if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
- $this->mContentModel = strval( $row->page_content_model );
+ $this->mContentModel = (string)$row->page_content_model;
} elseif ( !$this->mForcedContentModel ) {
$this->mContentModel = false; # initialized lazily in getContentModel()
}
$t = new Title();
$t->mInterwiki = $interwiki;
$t->mFragment = $fragment;
- $t->mNamespace = $ns = intval( $ns );
+ $t->mNamespace = $ns = (int)$ns;
$t->mDbkeyform = strtr( $title, ' ', '_' );
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
$t->mUrlform = wfUrlencode( $t->mDbkeyform );
/**
* Create a new Title for the Main Page
*
+ * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
+ * It may instead be a cached instance created previously, with references to it remaining
+ * elsewhere.
+ *
* @return Title The new object
*/
public static function newMainPage() {
// Allow unicode if a single high-bit character appears
$r0 = sprintf( '\x%02x', $ord0 );
$allowUnicode = true;
+ // @phan-suppress-next-line PhanParamSuspiciousOrder false positive
} elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
$r0 = '\\' . $d0;
} else {
return $name;
}
- /**
- * Escape a text fragment, say from a link, for a URL
- *
- * @deprecated since 1.30, use Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki()
- *
- * @param string $fragment Containing a URL or link fragment (after the "#")
- * @return string Escaped string
- */
- static function escapeFragmentForURL( $fragment ) {
- wfDeprecated( __METHOD__, '1.30' );
- # Note that we don't urlencode the fragment. urlencoded Unicode
- # fragments appear not to work in IE (at least up to 7) or in at least
- # one version of Opera 9.x. The W3C validator, for one, doesn't seem
- # to care if they aren't encoded.
- return Sanitizer::escapeId( $fragment, 'noninitial' );
- }
-
/**
* Callback for usort() to do title sorts by (namespace, title)
*
getNsText( MWNamespace::getTalk( $this->mNamespace ) );
}
- /**
- * Can this title have a corresponding talk page?
- *
- * @deprecated since 1.30, use canHaveTalkPage() instead.
- *
- * @return bool True if this title either is a talk page or can have a talk page associated.
- */
- public function canTalk() {
- return $this->canHaveTalkPage();
- }
-
/**
* Can this title have a corresponding talk page?
*
);
}
- /**
- * @return bool
- * @deprecated Since 1.31; use ::isSiteConfigPage() instead (which also checks for JSON pages)
- */
- public function isCssOrJsPage() {
- wfDeprecated( __METHOD__, '1.31' );
- return ( NS_MEDIAWIKI == $this->mNamespace
- && ( $this->hasContentModel( CONTENT_MODEL_CSS )
- || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
- }
-
/**
* Is this a "config" (.css, .json, or .js) sub-page of a user page?
*
);
}
- /**
- * @return bool
- * @deprecated Since 1.31; use ::isUserConfigPage() instead (which also checks for JSON pages)
- */
- public function isCssJsSubpage() {
- wfDeprecated( __METHOD__, '1.31' );
- return ( NS_USER == $this->mNamespace && $this->isSubpage()
- && ( $this->hasContentModel( CONTENT_MODEL_CSS )
- || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
- }
-
/**
* Trim down a .css, .json, or .js subpage title to get the corresponding skin name
*
return substr( $subpage, 0, $lastdot );
}
- /**
- * @deprecated Since 1.31; use ::getSkinFromConfigSubpage() instead
- * @return string Containing skin name from .css, .json, or .js subpage title
- */
- public function getSkinFromCssJsSubpage() {
- wfDeprecated( __METHOD__, '1.31' );
- return $this->getSkinFromConfigSubpage();
- }
-
/**
* Is this a CSS "config" sub-page of a user page?
*
);
}
- /**
- * @deprecated Since 1.31; use ::isUserCssConfigPage()
- * @return bool
- */
- public function isCssSubpage() {
- wfDeprecated( __METHOD__, '1.31' );
- return $this->isUserCssConfigPage();
- }
-
/**
* Is this a JSON "config" sub-page of a user page?
*
);
}
- /**
- * @deprecated Since 1.31; use ::isUserJsConfigPage()
- * @return bool
- */
- public function isJsSubpage() {
- wfDeprecated( __METHOD__, '1.31' );
- return $this->isUserJsConfigPage();
- }
-
/**
* Is this a sitewide CSS "config" page?
*
* @return string Base name
*/
public function getBaseText() {
+ $text = $this->getText();
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
- return $this->getText();
+ return $text;
}
- $parts = explode( '/', $this->getText() );
- # Don't discard the real title if there's no subpage involved
- if ( count( $parts ) > 1 ) {
- unset( $parts[count( $parts ) - 1] );
+ $lastSlashPos = strrpos( $text, '/' );
+ // Don't discard the real title if there's no subpage involved
+ if ( $lastSlashPos === false ) {
+ return $text;
}
- return implode( '/', $parts );
+
+ return substr( $text, 0, $lastSlashPos );
}
/**
* @endcode
*
* @param string $text The subpage name to add to the title
- * @return Title Subpage title
+ * @return Title|null Subpage title, or null on an error
* @since 1.20
*/
public function getSubpage( $text ) {
*
* @param string $action Action that permission needs to be checked for
* @param User|null $user User to check (since 1.19); $wgUser will be used if not provided.
+ *
* @return bool
+ * @throws Exception
+ *
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(..) instead
+ *
*/
public function quickUserCan( $action, $user = null ) {
return $this->userCan( $action, $user, false );
* @param User|null $user User to check (since 1.19); $wgUser will be used if not
* provided.
* @param string $rigor Same format as Title::getUserPermissionsErrors()
+ *
* @return bool
+ * @throws Exception
+ *
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->userCan(..) instead
+ *
*/
- public function userCan( $action, $user = null, $rigor = 'secure' ) {
+ public function userCan( $action, $user = null, $rigor = PermissionManager::RIGOR_SECURE ) {
if ( !$user instanceof User ) {
global $wgUser;
$user = $wgUser;
}
- return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
+ // TODO: this is for b/c, eventually will be removed
+ if ( $rigor === true ) {
+ $rigor = PermissionManager::RIGOR_SECURE; // b/c
+ } elseif ( $rigor === false ) {
+ $rigor = PermissionManager::RIGOR_QUICK; // b/c
+ }
+
+ return MediaWikiServices::getInstance()->getPermissionManager()
+ ->userCan( $action, $user, $this, $rigor );
}
/**
* - secure : does cheap and expensive checks, using the master as needed
* @param array $ignoreErrors Array of Strings Set this to a list of message keys
* whose corresponding errors may be ignored.
+ *
* @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
- */
- public function getUserPermissionsErrors(
- $action, $user, $rigor = 'secure', $ignoreErrors = []
- ) {
- $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
-
- // Remove the errors being ignored.
- foreach ( $errors as $index => $error ) {
- $errKey = is_array( $error ) ? $error[0] : $error;
-
- if ( in_array( $errKey, $ignoreErrors ) ) {
- unset( $errors[$index] );
- }
- if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
- unset( $errors[$index] );
- }
- }
-
- return $errors;
- }
-
- /**
- * Permissions checks that fail most often, and which are easiest to test.
+ * @throws Exception
*
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
+ * @deprecated since 1.33,
+ * use MediaWikiServices::getInstance()->getPermissionManager()->getUserPermissionsErrors()
*
- * @return array List of errors
*/
- private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
- if ( !Hooks::run( 'TitleQuickPermissions',
- [ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
- ) {
- return $errors;
- }
-
- if ( $action == 'create' ) {
- if (
- ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
- ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
- ) {
- $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ '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[] = [ '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[] = [ '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[] = [ '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[] = [ 'movenologintext' ];
- } else {
- $errors[] = [ 'movenotallowed' ];
- }
- }
- } elseif ( $action == 'move-target' ) {
- if ( !$user->isAllowed( 'move' ) ) {
- // User can't move anything
- $errors[] = [ '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[] = [ '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[] = [ 'cant-move-to-category-page' ];
- }
- } elseif ( !$user->isAllowed( $action ) ) {
- $errors[] = $this->missingPermissionError( $action, $short );
+ public function getUserPermissionsErrors(
+ $action, $user, $rigor = PermissionManager::RIGOR_SECURE, $ignoreErrors = []
+ ) {
+ // TODO: this is for b/c, eventually will be removed
+ if ( $rigor === true ) {
+ $rigor = PermissionManager::RIGOR_SECURE; // b/c
+ } elseif ( $rigor === false ) {
+ $rigor = PermissionManager::RIGOR_QUICK; // b/c
}
- return $errors;
+ return MediaWikiServices::getInstance()->getPermissionManager()
+ ->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors );
}
/**
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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
- // Use getUserPermissionsErrors instead
- $result = '';
- // Avoid PHP 7.1 warning from passing $this by reference
- $titleRef = $this;
- if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) {
- return $result ? [] : [ [ 'badaccess-group0' ] ];
- }
- // Check getUserPermissionsErrors hook
- // Avoid PHP 7.1 warning from passing $this by reference
- $titleRef = $this;
- if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) {
- $errors = $this->resultToError( $errors, $result );
- }
- // Check getUserPermissionsErrorsExpensive hook
- if (
- $rigor !== 'quick'
- && !( $short && count( $errors ) > 0 )
- && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
- # Only 'createaccount' can be performed on special pages,
- # which don't actually exist in the DB.
- if ( $this->isSpecialPage() && $action !== 'createaccount' ) {
- $errors[] = [ '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 ?
- [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
- }
-
- return $errors;
- }
-
- /**
- * Check sitewide CSS/JSON/JS permissions
- *
- * @param string $action The action to check
- * @param User $user User to check
- * @param array $errors List of current errors
- * @param string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short ) {
- if ( $action != 'patrol' ) {
- $error = null;
- // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
- // editinterface right. That's implemented as a restriction so no check needed here.
- if ( $this->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
- $error = [ 'sitecssprotected', $action ];
- } elseif ( $this->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
- $error = [ 'sitejsonprotected', $action ];
- } elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
- $error = [ 'sitejsprotected', $action ];
- } elseif ( $this->isRawHtmlMessage() ) {
- // Raw HTML can be used to deploy CSS or JS so require rights for both.
- if ( !$user->isAllowed( 'editsitejs' ) ) {
- $error = [ 'sitejsprotected', $action ];
- } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
- $error = [ 'sitecssprotected', $action ];
- }
- }
-
- if ( $error ) {
- if ( $user->isAllowed( 'editinterface' ) ) {
- // Most users / site admins will probably find out about the new, more restrictive
- // permissions by failing to edit something. Give them more info.
- // TODO remove this a few release cycles after 1.32
- $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
- }
- $errors[] = $error;
- }
- }
-
- return $errors;
- }
-
- /**
- * Check CSS/JSON/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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
- # Protect css/json/js subpages of user pages
- # XXX: this might be better using restrictions
-
- if ( $action === 'patrol' ) {
- return $errors;
- }
-
- if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
- // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
- if (
- $this->isUserCssConfigPage()
- && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
- ) {
- $errors[] = [ 'mycustomcssprotected', $action ];
- } elseif (
- $this->isUserJsonConfigPage()
- && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
- ) {
- $errors[] = [ 'mycustomjsonprotected', $action ];
- } elseif (
- $this->isUserJsConfigPage()
- && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
- ) {
- $errors[] = [ 'mycustomjsprotected', $action ];
- }
- } else {
- // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
- // deletion/suppression which cannot be used for attacks and we want to avoid the
- // situation where an unprivileged user can post abusive content on their subpages
- // and only very highly privileged users could remove it.
- if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
- if (
- $this->isUserCssConfigPage()
- && !$user->isAllowed( 'editusercss' )
- ) {
- $errors[] = [ 'customcssprotected', $action ];
- } elseif (
- $this->isUserJsonConfigPage()
- && !$user->isAllowed( 'edituserjson' )
- ) {
- $errors[] = [ 'customjsonprotected', $action ];
- } elseif (
- $this->isUserJsConfigPage()
- && !$user->isAllowed( 'edituserjs' )
- ) {
- $errors[] = [ '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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkPageRestrictions( $action, $user, $errors, $rigor, $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[] = [ 'protectedpagetext', $right, $action ];
- } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
- $errors[] = [ '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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
- if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
- # 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[] = [ '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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
- global $wgDeleteRevisionsLimit, $wgLang;
-
- if ( $action == 'protect' ) {
- if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
- // If they can't edit, they shouldn't protect.
- $errors[] = [ 'protect-cantedit' ];
- }
- } elseif ( $action == 'create' ) {
- $title_protection = $this->getTitleProtection();
- if ( $title_protection ) {
- if ( $title_protection['permission'] == ''
- || !$user->isAllowed( $title_protection['permission'] )
- ) {
- $errors[] = [
- 'titleprotected',
- User::whoIs( $title_protection['user'] ),
- $title_protection['reason']
- ];
- }
- }
- } elseif ( $action == 'move' ) {
- // Check for immobile pages
- if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
- // Specific message for this case
- $errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
- } elseif ( !$this->isMovable() ) {
- // Less specific message for rarer cases
- $errors[] = [ 'immobile-source-page' ];
- }
- } elseif ( $action == 'move-target' ) {
- if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
- $errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
- } elseif ( !$this->isMovable() ) {
- $errors[] = [ 'immobile-target-page' ];
- }
- } elseif ( $action == 'delete' ) {
- $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
- if ( !$tempErrors ) {
- $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
- $user, $tempErrors, $rigor, true );
- }
- if ( $tempErrors ) {
- // If protection keeps them from editing, they shouldn't be able to delete.
- $errors[] = [ 'deleteprotected' ];
- }
- if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
- && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
- ) {
- $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
- }
- } elseif ( $action === 'undelete' ) {
- if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
- // Undeleting implies editing
- $errors[] = [ 'undelete-cantedit' ];
- }
- if ( !$this->exists()
- && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
- ) {
- // Undeleting where nothing currently exists implies creating
- $errors[] = [ 'undelete-cantcreate' ];
- }
- }
- 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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
- global $wgEmailConfirmToEdit, $wgBlockDisablesLogin;
- // Account creation blocks handled at userlogin.
- // Unblocking handled in SpecialUnblock
- if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
- return $errors;
- }
-
- // Optimize for a very common case
- if ( $action === 'read' && !$wgBlockDisablesLogin ) {
- return $errors;
- }
-
- if ( $wgEmailConfirmToEdit
- && !$user->isEmailConfirmed()
- && $action === 'edit'
- ) {
- $errors[] = [ 'confirmedittext' ];
- }
-
- $useReplica = ( $rigor !== 'secure' );
- $block = $user->getBlock( $useReplica );
-
- // The block may explicitly allow an action (like "read" or "upload").
- if ( $block && $block->prevents( $action ) === false ) {
- return $errors;
- }
-
- // Determine if the user is blocked from this action on this page.
- // What gets passed into this method is a user right, not an action nmae.
- // There is no way to instantiate an action by restriction. However, this
- // will get the action where the restriction is the same. This may result
- // in actions being blocked that shouldn't be.
- if ( Action::exists( $action ) ) {
- // Clone the title to prevent mutations to this object which is done
- // by Title::loadFromRow() in WikiPage::loadFromRow().
- $page = WikiPage::factory( clone $this );
- // Creating an action will perform several database queries to ensure that
- // the action has not been overridden by the content type.
- // @todo FIXME: Pass the relevant context into this function.
- $action = Action::factory( $action, $page, RequestContext::getMain() );
- } else {
- $action = null;
- }
-
- // If no action object is returned, assume that the action requires unblock
- // which is the default.
- if ( !$action || $action->requiresUnblock() ) {
- if ( $user->isBlockedFrom( $this, $useReplica ) ) {
- // @todo FIXME: Pass the relevant context into this function.
- $errors[] = $block
- ? $block->getPermissionsError( RequestContext::getMain() )
- : [ 'actionblockedtext' ];
- }
- }
-
- 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 string $rigor Same format as Title::getUserPermissionsErrors()
- * @param bool $short Short circuit on first error
- *
- * @return array List of errors
- */
- private function checkReadPermissions( $action, $user, $errors, $rigor, $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( 'PasswordReset' )
- || $this->isSpecial( 'Userlogout' )
- ) {
- # 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->mNamespace == 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->mDbkeyform;
- list( $name, /* $subpage */ ) =
- MediaWikiServices::getInstance()->getSpecialPageFactory()->
- 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...
- Hooks::run( 'TitleReadWhitelist', [ $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 Array containing an error message key and any parameters
- */
- private function missingPermissionError( $action, $short ) {
- // We avoid expensive display logic for quickUserCan's and such
- if ( $short ) {
- return [ 'badaccess-group0' ];
- }
-
- return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
- }
-
- /**
- * Can $user perform $action on this page? This is an internal function,
- * with multiple levels of checks depending on performance needs; see $rigor below.
- * It does not check wfReadOnly().
- *
- * @param string $action Action that permission needs to be checked for
- * @param User $user User to check
- * @param string $rigor One of (quick,full,secure)
- * - quick : does cheap permission checks from replica DBs (usable for GUI creation)
- * - full : does cheap and expensive checks possibly from a replica DB
- * - secure : does cheap and expensive checks, using the master as needed
- * @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, $rigor = 'secure', $short = false
- ) {
- if ( $rigor === true ) {
- $rigor = 'secure'; // b/c
- } elseif ( $rigor === false ) {
- $rigor = 'quick'; // b/c
- } elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
- throw new Exception( "Invalid rigor parameter '$rigor'." );
- }
-
- # Read has special handling
- if ( $action == 'read' ) {
- $checks = [
- 'checkPermissionHooks',
- 'checkReadPermissions',
- 'checkUserBlock', // for wgBlockDisablesLogin
- ];
- # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
- # or checkUserConfigPermissions 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 = [
- 'checkQuickPermissions',
- 'checkPermissionHooks',
- 'checkPageRestrictions',
- 'checkCascadingSourcesRestrictions',
- 'checkActionPermissions',
- 'checkUserBlock'
- ];
- } else {
- $checks = [
- 'checkQuickPermissions',
- 'checkPermissionHooks',
- 'checkSpecialsAndNSPermissions',
- 'checkSiteConfigPermissions',
- 'checkUserConfigPermissions',
- 'checkPageRestrictions',
- 'checkCascadingSourcesRestrictions',
- 'checkActionPermissions',
- 'checkUserBlock'
- ];
- }
-
- $errors = [];
- foreach ( $checks as $method ) {
- $errors = $this->$method( $action, $user, $errors, $rigor, $short );
-
- if ( $short && $errors !== [] ) {
- break;
- }
- }
-
- 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
}
if ( $this->mOldRestrictions === false ) {
- $this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
- [ 'page_id' => $this->getArticleID() ], __METHOD__ );
+ $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+ $linkCache->addLinkObj( $this ); # in case we already had an article ID
+ $this->mOldRestrictions = $linkCache->getGoodLinkFieldObj( $this, 'restrictions' );
}
if ( $this->mOldRestrictions != '' ) {
$id = $this->getArticleID();
if ( $id ) {
$fname = __METHOD__;
- $loadRestrictionsFromDb = function ( Database $dbr ) use ( $fname, $id ) {
+ $loadRestrictionsFromDb = function ( IDatabase $dbr ) use ( $fname, $id ) {
return iterator_to_array(
$dbr->select(
'page_restrictions',
$dbr = wfGetDB( DB_MASTER );
$rows = $loadRestrictionsFromDb( $dbr );
} else {
- $cache = ObjectCache::getMainWANInstance();
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$rows = $cache->getWithSetCallback(
// Page protections always leave a new null revision
- $cache->makeKey( 'page-restrictions', $id, $this->getLatestRevID() ),
+ $cache->makeKey( 'page-restrictions', 'v1', $id, $this->getLatestRevID() ),
$cache::TTL_DAY,
function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
$dbr = wfGetDB( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ if ( $lb->hasOrMadeRecentMasterChanges() ) {
+ // @TODO: cleanup Title cache and caller assumption mess in general
+ $ttl = WANObjectCache::TTL_UNCACHEABLE;
+ }
return $loadRestrictionsFromDb( $dbr );
}
$linkCache->clearLink( $this );
$this->mArticleID = $linkCache->addLinkObj( $this );
$linkCache->forUpdate( $oldUpdate );
- } else {
- if ( $this->mArticleID == -1 ) {
- $this->mArticleID = $linkCache->addLinkObj( $this );
- }
+ } elseif ( $this->mArticleID == -1 ) {
+ $this->mArticleID = $linkCache->addLinkObj( $this );
}
return $this->mArticleID;
}
// @todo: get rid of secureAndSplit, refactor parsing code.
// @note: getTitleParser() returns a TitleParser implementation which does not have a
// splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
+ /** @var MediaWikiTitleCodec $titleCodec */
$titleCodec = MediaWikiServices::getInstance()->getTitleParser();
// MalformedTitleException can be thrown here
$parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace );
return $urls;
}
- /**
- * @deprecated since 1.27 use getCdnUrls()
- */
- public function getSquidURLs() {
- return $this->getCdnUrls();
- }
-
/**
* Purge all applicable CDN URLs
*/
return $errors ?: true;
}
- /**
- * Check if the requested move target is a valid file move target
- * @todo move this to MovePage
- * @param Title $nt Target title
- * @return array List of errors
- */
- protected function validateFileMoveOperation( $nt ) {
- global $wgUser;
-
- $errors = [];
-
- $destFile = wfLocalFile( $nt );
- $destFile->load( File::READ_LATEST );
- if ( !$wgUser->isAllowed( 'reupload-shared' )
- && !$destFile->exists() && wfFindFile( $nt )
- ) {
- $errors[] = [ 'file-exists-sharedrepo' ];
- }
-
- return $errors;
- }
-
/**
* Move a title to a new location
*
$dbw->onTransactionPreCommitOrIdle(
function () use ( $dbw ) {
ResourceLoaderWikiModule::invalidateModuleCache(
- $this, null, null, $dbw->getDomainId() );
+ $this, null, null, $dbw->getDomainID() );
},
__METHOD__
);
public function canUseNoindex() {
global $wgExemptFromUserRobotsControl;
- $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
- ? MWNamespace::getContentNamespaces()
- : $wgExemptFromUserRobotsControl;
+ $bannedNamespaces = $wgExemptFromUserRobotsControl ?? MWNamespace::getContentNamespaces();
return !in_array( $this->mNamespace, $bannedNamespaces );
}