summaryrefslogtreecommitdiff
path: root/includes/Title.php
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2012-05-03 13:01:35 +0200
committerPierre Schmitz <pierre@archlinux.de>2012-05-03 13:01:35 +0200
commitd9022f63880ce039446fba8364f68e656b7bf4cb (patch)
tree16b40fbf17bf7c9ee6f4ead25b16dd192378050a /includes/Title.php
parent27cf83d177256813e2e802241085fce5dd0f3fb9 (diff)
Update to MediaWiki 1.19.0
Diffstat (limited to 'includes/Title.php')
-rw-r--r--includes/Title.php2238
1 files changed, 1184 insertions, 1054 deletions
diff --git a/includes/Title.php b/includes/Title.php
index 02607496..f3cf79d4 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -47,7 +47,6 @@ class Title {
*/
const GAID_FOR_UPDATE = 1;
-
/**
* @name Private member variables
* Please use the accessor functions instead.
@@ -64,6 +63,7 @@ class Title {
var $mFragment; // /< Title fragment (i.e. the bit after the #)
var $mArticleID = -1; // /< Article ID, fetched from the link cache on demand
var $mLatestID = false; // /< ID of most recent revision
+ private $mEstimateRevisions; // /< Estimated number of revisions; null of not loaded
var $mRestrictions = array(); // /< Array of groups allowed to edit this article
var $mOldRestrictions = false;
var $mCascadeRestriction; ///< Cascade restrictions on this page to included templates and images?
@@ -388,8 +388,8 @@ class Title {
$titles = array( $title );
while ( --$recurse > 0 ) {
if ( $title->isRedirect() ) {
- $article = new Article( $title, 0 );
- $newtitle = $article->getRedirectTarget();
+ $page = WikiPage::factory( $title );
+ $newtitle = $page->getRedirectTarget();
} else {
break;
}
@@ -443,10 +443,6 @@ class Title {
return null;
}
-# ----------------------------------------------------------------------------
-# Static functions
-# ----------------------------------------------------------------------------
-
/**
* Get the prefixed DB key associated with an ID
*
@@ -481,6 +477,33 @@ class Title {
}
/**
+ * Returns a simple regex that will match on characters and sequences invalid in titles.
+ * Note that this doesn't pick up many things that could be wrong with titles, but that
+ * replacing this regex with something valid will make many titles valid.
+ *
+ * @return String regex string
+ */
+ static function getTitleInvalidRegex() {
+ static $rxTc = false;
+ if ( !$rxTc ) {
+ # Matching titles will be held as illegal.
+ $rxTc = '/' .
+ # Any character not allowed is forbidden...
+ '[^' . self::legalChars() . ']' .
+ # URL percent encoding sequences interfere with the ability
+ # to round-trip titles -- you can't link to them consistently.
+ '|%[0-9A-Fa-f]{2}' .
+ # XML/HTML character references produce similar issues.
+ '|&[A-Za-z0-9\x80-\xff]+;' .
+ '|&#[0-9]+;' .
+ '|&#x[0-9A-Fa-f]+;' .
+ '/S';
+ }
+
+ return $rxTc;
+ }
+
+ /**
* Get a string representation of a title suitable for
* including in a search index
*
@@ -532,6 +555,36 @@ class Title {
}
/**
+ * Escape a text fragment, say from a link, for a URL
+ *
+ * @param $fragment string containing a URL or link fragment (after the "#")
+ * @return String: escaped string
+ */
+ static function escapeFragmentForURL( $fragment ) {
+ # 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)
+ *
+ * @param $a Title
+ * @param $b Title
+ *
+ * @return Integer: result of string comparison, or namespace comparison
+ */
+ public static function compare( $a, $b ) {
+ if ( $a->getNamespace() == $b->getNamespace() ) {
+ return strcmp( $a->getText(), $b->getText() );
+ } else {
+ return $a->getNamespace() - $b->getNamespace();
+ }
+ }
+
+ /**
* Determine whether the object refers to a page within
* this project.
*
@@ -546,6 +599,24 @@ class Title {
}
/**
+ * Is this Title interwiki?
+ *
+ * @return Bool
+ */
+ public function isExternal() {
+ return ( $this->mInterwiki != '' );
+ }
+
+ /**
+ * Get the interwiki prefix (or null string)
+ *
+ * @return String Interwiki prefix
+ */
+ public function getInterwiki() {
+ return $this->mInterwiki;
+ }
+
+ /**
* Determine whether the object refers to a page within
* this project and is transcludable.
*
@@ -573,51 +644,49 @@ class Title {
}
/**
- * Escape a text fragment, say from a link, for a URL
- *
- * @param $fragment string containing a URL or link fragment (after the "#")
- * @return String: escaped string
- */
- static function escapeFragmentForURL( $fragment ) {
- # 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' );
- }
-
-# ----------------------------------------------------------------------------
-# Other stuff
-# ----------------------------------------------------------------------------
-
- /** Simple accessors */
- /**
* Get the text form (spaces not underscores) of the main part
*
* @return String Main part of the title
*/
- public function getText() { return $this->mTextform; }
+ public function getText() {
+ return $this->mTextform;
+ }
/**
* Get the URL-encoded form of the main part
*
* @return String Main part of the title, URL-encoded
*/
- public function getPartialURL() { return $this->mUrlform; }
+ public function getPartialURL() {
+ return $this->mUrlform;
+ }
/**
* Get the main part with underscores
*
* @return String: Main part of the title, with underscores
*/
- public function getDBkey() { return $this->mDbkeyform; }
+ public function getDBkey() {
+ return $this->mDbkeyform;
+ }
+
+ /**
+ * Get the DB key with the initial letter case as specified by the user
+ *
+ * @return String DB key
+ */
+ function getUserCaseDBKey() {
+ return $this->mUserCaseDBKey;
+ }
/**
* Get the namespace index, i.e. one of the NS_xxxx constants.
*
* @return Integer: Namespace index
*/
- public function getNamespace() { return $this->mNamespace; }
+ public function getNamespace() {
+ return $this->mNamespace;
+ }
/**
* Get the namespace text
@@ -657,15 +726,6 @@ class Title {
}
/**
- * Get the DB key with the initial letter case as specified by the user
- *
- * @return String DB key
- */
- function getUserCaseDBKey() {
- return $this->mUserCaseDBKey;
- }
-
- /**
* Get the namespace text of the subject (rather than talk) page
*
* @return String Namespace text
@@ -695,18 +755,306 @@ class Title {
}
/**
- * Get the interwiki prefix (or null string)
+ * Is this in a namespace that allows actual pages?
*
- * @return String Interwiki prefix
+ * @return Bool
+ * @internal note -- uses hardcoded namespace index instead of constants
+ */
+ public function canExist() {
+ return $this->mNamespace >= NS_MAIN;
+ }
+
+ /**
+ * Can this title be added to a user's watchlist?
+ *
+ * @return Bool TRUE or FALSE
+ */
+ public function isWatchable() {
+ return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
+ }
+
+ /**
+ * Returns true if this is a special page.
+ *
+ * @return boolean
+ */
+ public function isSpecialPage() {
+ return $this->getNamespace() == NS_SPECIAL;
+ }
+
+ /**
+ * Returns true if this title resolves to the named special page
+ *
+ * @param $name String The special page name
+ * @return boolean
*/
- public function getInterwiki() { return $this->mInterwiki; }
+ public function isSpecial( $name ) {
+ if ( $this->isSpecialPage() ) {
+ list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
+ if ( $name == $thisName ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * If the Title refers to a special page alias which is not the local default, resolve
+ * the alias, and localise the name as necessary. Otherwise, return $this
+ *
+ * @return Title
+ */
+ public function fixSpecialName() {
+ if ( $this->isSpecialPage() ) {
+ list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
+ if ( $canonicalName ) {
+ $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
+ if ( $localName != $this->mDbkeyform ) {
+ return Title::makeTitle( NS_SPECIAL, $localName );
+ }
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Returns true if the title is inside the specified namespace.
+ *
+ * Please make use of this instead of comparing to getNamespace()
+ * This function is much more resistant to changes we may make
+ * to namespaces than code that makes direct comparisons.
+ * @param $ns int The namespace
+ * @return bool
+ * @since 1.19
+ */
+ public function inNamespace( $ns ) {
+ return MWNamespace::equals( $this->getNamespace(), $ns );
+ }
+
+ /**
+ * Returns true if the title is inside one of the specified namespaces.
+ *
+ * @param ...$namespaces The namespaces to check for
+ * @return bool
+ * @since 1.19
+ */
+ public function inNamespaces( /* ... */ ) {
+ $namespaces = func_get_args();
+ if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
+ $namespaces = $namespaces[0];
+ }
+
+ foreach ( $namespaces as $ns ) {
+ if ( $this->inNamespace( $ns ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the title has the same subject namespace as the
+ * namespace specified.
+ * For example this method will take NS_USER and return true if namespace
+ * is either NS_USER or NS_USER_TALK since both of them have NS_USER
+ * as their subject namespace.
+ *
+ * This is MUCH simpler than individually testing for equivilance
+ * against both NS_USER and NS_USER_TALK, and is also forward compatible.
+ * @since 1.19
+ */
+ public function hasSubjectNamespace( $ns ) {
+ return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
+ }
+
+ /**
+ * Is this Title in a namespace which contains content?
+ * In other words, is this a content page, for the purposes of calculating
+ * statistics, etc?
+ *
+ * @return Boolean
+ */
+ public function isContentPage() {
+ return MWNamespace::isContent( $this->getNamespace() );
+ }
+
+ /**
+ * Would anybody with sufficient privileges be able to move this page?
+ * Some pages just aren't movable.
+ *
+ * @return Bool TRUE or FALSE
+ */
+ public function isMovable() {
+ if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) {
+ // Interwiki title or immovable namespace. Hooks don't get to override here
+ return false;
+ }
+
+ $result = true;
+ wfRunHooks( 'TitleIsMovable', array( $this, &$result ) );
+ return $result;
+ }
+
+ /**
+ * Is this the mainpage?
+ * @note Title::newFromText seams to be sufficiently optimized by the title
+ * cache that we don't need to over-optimize by doing direct comparisons and
+ * acidentally creating new bugs where $title->equals( Title::newFromText() )
+ * ends up reporting something differently than $title->isMainPage();
+ *
+ * @since 1.18
+ * @return Bool
+ */
+ public function isMainPage() {
+ return $this->equals( Title::newMainPage() );
+ }
+
+ /**
+ * Is this a subpage?
+ *
+ * @return Bool
+ */
+ public function isSubpage() {
+ return MWNamespace::hasSubpages( $this->mNamespace )
+ ? strpos( $this->getText(), '/' ) !== false
+ : false;
+ }
+
+ /**
+ * Is this a conversion table for the LanguageConverter?
+ *
+ * @return Bool
+ */
+ public function isConversionTable() {
+ return $this->getNamespace() == NS_MEDIAWIKI &&
+ strpos( $this->getText(), 'Conversiontable' ) !== false;
+ }
+
+ /**
+ * Does that page contain wikitext, or it is JS, CSS or whatever?
+ *
+ * @return Bool
+ */
+ public function isWikitextPage() {
+ $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
+ wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
+ return $retval;
+ }
+
+ /**
+ * Could this page contain custom CSS or JavaScript, based
+ * on the title?
+ *
+ * @return Bool
+ */
+ public function isCssOrJsPage() {
+ $retval = $this->mNamespace == NS_MEDIAWIKI
+ && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
+ wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
+ return $retval;
+ }
+
+ /**
+ * Is this a .css or .js subpage of a user page?
+ * @return Bool
+ */
+ public function isCssJsSubpage() {
+ return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
+ }
+
+ /**
+ * Trim down a .css or .js subpage title to get the corresponding skin name
+ *
+ * @return string containing skin name from .css or .js subpage title
+ */
+ public function getSkinFromCssJsSubpage() {
+ $subpage = explode( '/', $this->mTextform );
+ $subpage = $subpage[ count( $subpage ) - 1 ];
+ $lastdot = strrpos( $subpage, '.' );
+ if ( $lastdot === false )
+ return $subpage; # Never happens: only called for names ending in '.css' or '.js'
+ return substr( $subpage, 0, $lastdot );
+ }
+
+ /**
+ * Is this a .css subpage of a user page?
+ *
+ * @return Bool
+ */
+ public function isCssSubpage() {
+ return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
+ }
+
+ /**
+ * Is this a .js subpage of a user page?
+ *
+ * @return Bool
+ */
+ public function isJsSubpage() {
+ return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+ }
+
+ /**
+ * Is this a talk page of some sort?
+ *
+ * @return Bool
+ */
+ public function isTalkPage() {
+ return MWNamespace::isTalk( $this->getNamespace() );
+ }
+
+ /**
+ * Get a Title object associated with the talk page of this article
+ *
+ * @return Title the object for the talk page
+ */
+ public function getTalkPage() {
+ return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
+ }
+
+ /**
+ * Get a title object associated with the subject page of this
+ * talk page
+ *
+ * @return Title the object for the subject page
+ */
+ public function getSubjectPage() {
+ // Is this the same title?
+ $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
+ if ( $this->getNamespace() == $subjectNS ) {
+ return $this;
+ }
+ return Title::makeTitle( $subjectNS, $this->getDBkey() );
+ }
+
+ /**
+ * Get the default namespace index, for when there is no namespace
+ *
+ * @return Int Default namespace index
+ */
+ public function getDefaultNamespace() {
+ return $this->mDefaultNamespace;
+ }
+
+ /**
+ * Get title for search index
+ *
+ * @return String a stripped-down title string ready for the
+ * search index
+ */
+ public function getIndexTitle() {
+ return Title::indexTitle( $this->mNamespace, $this->mTextform );
+ }
/**
* Get the Title fragment (i.e.\ the bit after the #) in text form
*
* @return String Title fragment
*/
- public function getFragment() { return $this->mFragment; }
+ public function getFragment() {
+ return $this->mFragment;
+ }
/**
* Get the fragment in URL form, including the "#" character if there is one
@@ -721,20 +1069,37 @@ class Title {
}
/**
- * Get the default namespace index, for when there is no namespace
+ * Set the fragment for this title. Removes the first character from the
+ * specified fragment before setting, so it assumes you're passing it with
+ * an initial "#".
*
- * @return Int Default namespace index
+ * Deprecated for public use, use Title::makeTitle() with fragment parameter.
+ * Still in active use privately.
+ *
+ * @param $fragment String text
*/
- public function getDefaultNamespace() { return $this->mDefaultNamespace; }
+ public function setFragment( $fragment ) {
+ $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
+ }
/**
- * Get title for search index
+ * Prefix some arbitrary text with the namespace or interwiki prefix
+ * of this object
*
- * @return String a stripped-down title string ready for the
- * search index
+ * @param $name String the text
+ * @return String the prefixed text
+ * @private
*/
- public function getIndexTitle() {
- return Title::indexTitle( $this->mNamespace, $this->mTextform );
+ private function prefix( $name ) {
+ $p = '';
+ if ( $this->mInterwiki != '' ) {
+ $p = $this->mInterwiki . ':';
+ }
+
+ if ( 0 != $this->mNamespace ) {
+ $p .= $this->getNsText() . ':';
+ }
+ return $p . $name;
}
/**
@@ -766,6 +1131,15 @@ class Title {
}
/**
+ * Return a string representation of this title
+ *
+ * @return String representation of this title
+ */
+ public function __toString() {
+ return $this->getPrefixedText();
+ }
+
+ /**
* Get the prefixed title with spaces, plus any fragment
* (part beginning with '#')
*
@@ -811,6 +1185,17 @@ class Title {
}
/**
+ * Get the HTML-escaped displayable text form.
+ * Used for the title field in <a> tags.
+ *
+ * @return String the text, including any prefixes
+ */
+ public function getEscapedText() {
+ wfDeprecated( __METHOD__, '1.19' );
+ return htmlspecialchars( $this->getPrefixedText() );
+ }
+
+ /**
* Get a URL-encoded form of the subpage text
*
* @return String URL-encoded subpage name
@@ -833,48 +1218,64 @@ class Title {
}
/**
- * Get a real URL referring to this title, with interwiki link and
- * fragment
+ * Helper to fix up the get{Local,Full,Link,Canonical}URL args
+ * get{Canonical,Full,Link,Local}URL methods accepted an optional
+ * second argument named variant. This was deprecated in favor
+ * of passing an array of option with a "variant" key
+ * Once $query2 is removed for good, this helper can be dropped
+ * andthe wfArrayToCGI moved to getLocalURL();
*
- * @param $query \twotypes{\string,\array} an optional query string, not used for interwiki
- * links. Can be specified as an associative array as well, e.g.,
- * array( 'action' => 'edit' ) (keys and values will be URL-escaped).
- * @param $variant String language variant of url (for sr, zh..)
- * @return String the URL
+ * @since 1.19 (r105919)
*/
- public function getFullURL( $query = '', $variant = false ) {
- global $wgServer, $wgRequest;
-
+ private static function fixUrlQueryArgs( $query, $query2 = false ) {
+ if( $query2 !== false ) {
+ wfDeprecated( "Title::get{Canonical,Full,Link,Local} method called with a second parameter is deprecated. Add your parameter to an array passed as the first parameter.", "1.19" );
+ }
if ( is_array( $query ) ) {
$query = wfArrayToCGI( $query );
}
-
- $interwiki = Interwiki::fetch( $this->mInterwiki );
- if ( !$interwiki ) {
- $url = $this->getLocalURL( $query, $variant );
-
- // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc)
- // Correct fix would be to move the prepending elsewhere.
- if ( $wgRequest->getVal( 'action' ) != 'render' ) {
- $url = $wgServer . $url;
+ if ( $query2 ) {
+ if ( is_string( $query2 ) ) {
+ // $query2 is a string, we will consider this to be
+ // a deprecated $variant argument and add it to the query
+ $query2 = wfArrayToCGI( array( 'variant' => $query2 ) );
+ } else {
+ $query2 = wfArrayToCGI( $query2 );
}
- } else {
- $baseUrl = $interwiki->getURL();
-
- $namespace = wfUrlencode( $this->getNsText() );
- if ( $namespace != '' ) {
- # Can this actually happen? Interwikis shouldn't be parsed.
- # Yes! It can in interwiki transclusion. But... it probably shouldn't.
- $namespace .= ':';
+ // If we have $query content add a & to it first
+ if ( $query ) {
+ $query .= '&';
}
- $url = str_replace( '$1', $namespace . $this->mUrlform, $baseUrl );
- $url = wfAppendQuery( $url, $query );
+ // Now append the queries together
+ $query .= $query2;
}
+ return $query;
+ }
+
+ /**
+ * Get a real URL referring to this title, with interwiki link and
+ * fragment
+ *
+ * See getLocalURL for the arguments.
+ *
+ * @see self::getLocalURL
+ * @return String the URL
+ */
+ public function getFullURL( $query = '', $query2 = false ) {
+ $query = self::fixUrlQueryArgs( $query, $query2 );
+
+ # Hand off all the decisions on urls to getLocalURL
+ $url = $this->getLocalURL( $query );
+
+ # Expand the url to make it a full url. Note that getLocalURL has the
+ # potential to output full urls for a variety of reasons, so we use
+ # wfExpandUrl instead of simply prepending $wgServer
+ $url = wfExpandUrl( $url, PROTO_RELATIVE );
# Finally, add the fragment.
$url .= $this->getFragmentForURL();
- wfRunHooks( 'GetFullURL', array( &$this, &$url, $query, $variant ) );
+ wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
return $url;
}
@@ -882,48 +1283,45 @@ class Title {
* Get a URL with no fragment or server name. If this page is generated
* with action=render, $wgServer is prepended.
*
- * @param $query Mixed: an optional query string; if not specified,
- * $wgArticlePath will be used. Can be specified as an associative array
- * as well, e.g., array( 'action' => 'edit' ) (keys and values will be
- * URL-escaped).
- * @param $variant String language variant of url (for sr, zh..)
+
+ * @param $query \twotypes{\string,\array} an optional query string,
+ * not used for interwiki links. Can be specified as an associative array as well,
+ * e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
+ * Some query patterns will trigger various shorturl path replacements.
+ * @param $query2 Mixed: An optional secondary query array. This one MUST
+ * be an array. If a string is passed it will be interpreted as a deprecated
+ * variant argument and urlencoded into a variant= argument.
+ * This second query argument will be added to the $query
+ * The second parameter is deprecated since 1.19. Pass it as a key,value
+ * pair in the first parameter array instead.
+ *
* @return String the URL
*/
- public function getLocalURL( $query = '', $variant = false ) {
+ public function getLocalURL( $query = '', $query2 = false ) {
global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
- global $wgVariantArticlePath, $wgContLang;
- if ( is_array( $query ) ) {
- $query = wfArrayToCGI( $query );
- }
+ $query = self::fixUrlQueryArgs( $query, $query2 );
- if ( $this->isExternal() ) {
- $url = $this->getFullURL();
- if ( $query ) {
- // This is currently only used for edit section links in the
- // context of interwiki transclusion. In theory we should
- // append the query to the end of any existing query string,
- // but interwiki transclusion is already broken in that case.
- $url .= "?$query";
+ $interwiki = Interwiki::fetch( $this->mInterwiki );
+ if ( $interwiki ) {
+ $namespace = $this->getNsText();
+ if ( $namespace != '' ) {
+ # Can this actually happen? Interwikis shouldn't be parsed.
+ # Yes! It can in interwiki transclusion. But... it probably shouldn't.
+ $namespace .= ':';
}
+ $url = $interwiki->getURL( $namespace . $this->getDBkey() );
+ $url = wfAppendQuery( $url, $query );
} else {
$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
if ( $query == '' ) {
- if ( $variant != false && $wgContLang->hasVariants() ) {
- if ( !$wgVariantArticlePath ) {
- $variantArticlePath = "$wgScript?title=$1&variant=$2"; // default
- } else {
- $variantArticlePath = $wgVariantArticlePath;
- }
- $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath );
- $url = str_replace( '$1', $dbkey, $url );
- } else {
- $url = str_replace( '$1', $dbkey, $wgArticlePath );
- }
+ $url = str_replace( '$1', $dbkey, $wgArticlePath );
+ wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) );
} else {
- global $wgActionPaths;
+ global $wgVariantArticlePath, $wgActionPaths;
$url = false;
$matches = array();
+
if ( !empty( $wgActionPaths ) &&
preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) )
{
@@ -940,6 +1338,20 @@ class Title {
}
}
+ if ( $url === false &&
+ $wgVariantArticlePath &&
+ $this->getPageLanguage()->hasVariants() &&
+ preg_match( '/^variant=([^&]*)$/', $query, $matches ) )
+ {
+ $variant = urldecode( $matches[1] );
+ if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
+ // Only do the variant replacement if the given variant is a valid
+ // variant for the page's language.
+ $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
+ $url = str_replace( '$1', $dbkey, $url );
+ }
+ }
+
if ( $url === false ) {
if ( $query == '-' ) {
$query = '';
@@ -948,6 +1360,8 @@ class Title {
}
}
+ wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) );
+
// @todo FIXME: This causes breakage in various places when we
// actually expected a local URL and end up with dupe prefixes.
if ( $wgRequest->getVal( 'action' ) == 'render' ) {
@@ -968,21 +1382,19 @@ class Title {
* The result obviously should not be URL-escaped, but does need to be
* HTML-escaped if it's being output in HTML.
*
- * @param $query Array of Strings An associative array of key => value pairs for the
- * query string. Keys and values will be escaped.
- * @param $variant String language variant of URL (for sr, zh..). Ignored
- * for external links. Default is "false" (same variant as current page,
- * for anonymous users).
+ * See getLocalURL for the arguments.
+ *
+ * @see self::getLocalURL
* @return String the URL
*/
- public function getLinkUrl( $query = array(), $variant = false ) {
+ public function getLinkURL( $query = '', $query2 = false ) {
wfProfileIn( __METHOD__ );
if ( $this->isExternal() ) {
- $ret = $this->getFullURL( $query );
+ $ret = $this->getFullURL( $query, $query2 );
} elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
$ret = $this->getFragmentForURL();
} else {
- $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL();
+ $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
}
wfProfileOut( __METHOD__ );
return $ret;
@@ -992,49 +1404,50 @@ class Title {
* Get an HTML-escaped version of the URL form, suitable for
* using in a link, without a server name or fragment
*
- * @param $query String an optional query string
+ * See getLocalURL for the arguments.
+ *
+ * @see self::getLocalURL
* @return String the URL
*/
- public function escapeLocalURL( $query = '' ) {
- return htmlspecialchars( $this->getLocalURL( $query ) );
+ public function escapeLocalURL( $query = '', $query2 = false ) {
+ wfDeprecated( __METHOD__, '1.19' );
+ return htmlspecialchars( $this->getLocalURL( $query, $query2 ) );
}
/**
* Get an HTML-escaped version of the URL form, suitable for
* using in a link, including the server name and fragment
*
- * @param $query String an optional query string
+ * See getLocalURL for the arguments.
+ *
+ * @see self::getLocalURL
* @return String the URL
*/
- public function escapeFullURL( $query = '' ) {
- return htmlspecialchars( $this->getFullURL( $query ) );
- }
-
- /**
- * HTML-escaped version of getCanonicalURL()
- */
- public function escapeCanonicalURL( $query = '', $variant = false ) {
- return htmlspecialchars( $this->getCanonicalURL( $query, $variant ) );
+ public function escapeFullURL( $query = '', $query2 = false ) {
+ wfDeprecated( __METHOD__, '1.19' );
+ return htmlspecialchars( $this->getFullURL( $query, $query2 ) );
}
/**
* Get the URL form for an internal link.
* - Used in various Squid-related code, in case we have a different
* internal hostname for the server from the exposed one.
- *
+ *
* This uses $wgInternalServer to qualify the path, or $wgServer
* if $wgInternalServer is not set. If the server variable used is
* protocol-relative, the URL will be expanded to http://
*
- * @param $query String an optional query string
- * @param $variant String language variant of url (for sr, zh..)
+ * See getLocalURL for the arguments.
+ *
+ * @see self::getLocalURL
* @return String the URL
*/
- public function getInternalURL( $query = '', $variant = false ) {
+ public function getInternalURL( $query = '', $query2 = false ) {
global $wgInternalServer, $wgServer;
+ $query = self::fixUrlQueryArgs( $query, $query2 );
$server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
- $url = wfExpandUrl( $server . $this->getLocalURL( $query, $variant ), PROTO_HTTP );
- wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query, $variant ) );
+ $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
+ wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
return $url;
}
@@ -1042,21 +1455,36 @@ class Title {
* Get the URL for a canonical link, for use in things like IRC and
* e-mail notifications. Uses $wgCanonicalServer and the
* GetCanonicalURL hook.
- *
+ *
* NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
- *
- * @param $query string An optional query string
- * @param $variant string Language variant of URL (for sr, zh, ...)
+ *
+ * See getLocalURL for the arguments.
+ *
+ * @see self::getLocalURL
* @return string The URL
+ * @since 1.18
*/
- public function getCanonicalURL( $query = '', $variant = false ) {
- global $wgCanonicalServer;
- $url = wfExpandUrl( $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(), PROTO_CANONICAL );
- wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query, $variant ) );
+ public function getCanonicalURL( $query = '', $query2 = false ) {
+ $query = self::fixUrlQueryArgs( $query, $query2 );
+ $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
+ wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) );
return $url;
}
/**
+ * HTML-escaped version of getCanonicalURL()
+ *
+ * See getLocalURL for the arguments.
+ *
+ * @see self::getLocalURL
+ * @since 1.18
+ */
+ public function escapeCanonicalURL( $query = '', $query2 = false ) {
+ wfDeprecated( __METHOD__, '1.19' );
+ return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) );
+ }
+
+ /**
* Get the edit URL for this Title
*
* @return String the URL, or a null string if this is an
@@ -1072,100 +1500,6 @@ class Title {
}
/**
- * Get the HTML-escaped displayable text form.
- * Used for the title field in <a> tags.
- *
- * @return String the text, including any prefixes
- */
- public function getEscapedText() {
- return htmlspecialchars( $this->getPrefixedText() );
- }
-
- /**
- * Is this Title interwiki?
- *
- * @return Bool
- */
- public function isExternal() {
- return ( $this->mInterwiki != '' );
- }
-
- /**
- * Is this page "semi-protected" - the *only* protection is autoconfirm?
- *
- * @param $action String Action to check (default: edit)
- * @return Bool
- */
- public function isSemiProtected( $action = 'edit' ) {
- if ( $this->exists() ) {
- $restrictions = $this->getRestrictions( $action );
- if ( count( $restrictions ) > 0 ) {
- foreach ( $restrictions as $restriction ) {
- if ( strtolower( $restriction ) != 'autoconfirmed' ) {
- return false;
- }
- }
- } else {
- # Not protected
- return false;
- }
- return true;
- } else {
- # If it doesn't exist, it can't be protected
- return false;
- }
- }
-
- /**
- * Does the title correspond to a protected article?
- *
- * @param $action String 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->getNamespace() == NS_SPECIAL ) {
- 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;
- }
-
- /**
- * Is this a conversion table for the LanguageConverter?
- *
- * @return Bool
- */
- public function isConversionTable() {
- if(
- $this->getNamespace() == NS_MEDIAWIKI &&
- strpos( $this->getText(), 'Conversiontable' ) !== false
- )
- {
- return true;
- }
-
- return false;
- }
-
- /**
* Is $wgUser watching this page?
*
* @return Bool
@@ -1184,7 +1518,19 @@ class Title {
}
/**
- * Can $wgUser perform $action on this page?
+ * Can $wgUser read this page?
+ *
+ * @deprecated in 1.19; use userCan(), quickUserCan() or getUserPermissionsErrors() instead
+ * @return Bool
+ * @todo fold these checks into userCan()
+ */
+ public function userCanRead() {
+ wfDeprecated( __METHOD__, '1.19' );
+ return $this->userCan( 'read' );
+ }
+
+ /**
+ * Can $user perform $action on this page?
* This skips potentially expensive cascading permission checks
* as well as avoids expensive error formatting
*
@@ -1194,47 +1540,30 @@ class Title {
* May provide false positives, but should never provide a false negative.
*
* @param $action String action that permission needs to be checked for
+ * @param $user User to check (since 1.19); $wgUser will be used if not
+ * provided.
* @return Bool
*/
- public function quickUserCan( $action ) {
- return $this->userCan( $action, false );
+ public function quickUserCan( $action, $user = null ) {
+ return $this->userCan( $action, $user, false );
}
/**
- * Determines if $user is unable to edit this page because it has been protected
- * by $wgNamespaceProtection.
+ * Can $user perform $action on this page?
*
- * @param $user User object, $wgUser will be used if not passed
+ * @param $action String action that permission needs to be checked for
+ * @param $user User to check (since 1.19); $wgUser will be used if not
+ * provided.
+ * @param $doExpensiveQueries Bool Set this to false to avoid doing
+ * unnecessary queries.
* @return Bool
*/
- public function isNamespaceProtected( User $user = null ) {
- global $wgNamespaceProtection;
-
- if ( $user === null ) {
+ public function userCan( $action, $user = null, $doExpensiveQueries = true ) {
+ if ( !$user instanceof User ) {
global $wgUser;
$user = $wgUser;
}
-
- if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
- foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
- if ( $right != '' && !$user->isAllowed( $right ) ) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * Can $wgUser perform $action on this page?
- *
- * @param $action String action that permission needs to be checked for
- * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries.
- * @return Bool
- */
- public function userCan( $action, $doExpensiveQueries = true ) {
- global $wgUser;
- return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array() );
+ return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) );
}
/**
@@ -1244,9 +1573,10 @@ class Title {
*
* @param $action String action that permission needs to be checked for
* @param $user User to check
- * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries by
- * skipping checks for cascading protections and user blocks.
- * @param $ignoreErrors Array of Strings Set this to a list of message keys whose corresponding errors may be ignored.
+ * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary
+ * queries by skipping checks for cascading protections and user blocks.
+ * @param $ignoreErrors Array of Strings Set this to a list of message keys
+ * whose corresponding errors may be ignored.
* @return Array of arguments to wfMsg to explain permissions problems.
*/
public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) {
@@ -1321,24 +1651,7 @@ class Title {
$errors[] = array( 'cant-move-to-user-page' );
}
} elseif ( !$user->isAllowed( $action ) ) {
- // We avoid expensive display logic for quickUserCan's and such
- $groups = false;
- if ( !$short ) {
- $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
- User::getGroupsWithPermission( $action ) );
- }
-
- if ( $groups ) {
- global $wgLang;
- $return = array(
- 'badaccess-groups',
- $wgLang->commaList( $groups ),
- count( $groups )
- );
- } else {
- $return = array( 'badaccess-group0' );
- }
- $errors[] = $return;
+ $errors[] = $this->missingPermissionError( $action, $short );
}
return $errors;
@@ -1413,7 +1726,7 @@ class Title {
private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
# Only 'createaccount' and 'execute' can be performed on
# special pages, which don't actually exist in the DB.
- $specialOKActions = array( 'createaccount', 'execute' );
+ $specialOKActions = array( 'createaccount', 'execute', 'read' );
if ( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions ) ) {
$errors[] = array( 'ns-specialprotected' );
}
@@ -1443,8 +1756,6 @@ class Title {
private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
# Protect css/js subpages of user pages
# XXX: this might be better using restrictions
- # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssSubpage()
- # and $this->userCanEditJsSubpage() from working
# XXX: right 'editusercssjs' is deprecated, for backward compatibility only
if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' )
&& !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
@@ -1479,13 +1790,10 @@ class Title {
}
if ( $right != '' && !$user->isAllowed( $right ) ) {
// Users with 'editprotected' permission can edit protected pages
- if ( $action == 'edit' && $user->isAllowed( 'editprotected' ) ) {
- // Users with 'editprotected' permission cannot edit protected pages
- // with cascading option turned on.
- if ( $this->mCascadeRestriction ) {
- $errors[] = array( 'protectedpagetext', $right );
- }
- } else {
+ // without cascading option turned on.
+ if ( $action != 'edit' || !$user->isAllowed( 'editprotected' )
+ || $this->mCascadeRestriction )
+ {
$errors[] = array( 'protectedpagetext', $right );
}
}
@@ -1545,8 +1853,10 @@ class Title {
* @return Array list of errors
*/
private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
+ global $wgDeleteRevisionsLimit, $wgLang;
+
if ( $action == 'protect' ) {
- if ( $this->getUserPermissionsErrors( 'edit', $user ) != array() ) {
+ if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) {
// If they can't edit, they shouldn't protect.
$errors[] = array( 'protect-cantedit' );
}
@@ -1556,7 +1866,9 @@ class Title {
if( $title_protection['pt_create_perm'] == 'sysop' ) {
$title_protection['pt_create_perm'] = 'protect'; // B/C
}
- if( $title_protection['pt_create_perm'] == '' || !$user->isAllowed( $title_protection['pt_create_perm'] ) ) {
+ 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'] );
}
}
@@ -1567,7 +1879,7 @@ class Title {
$errors[] = array( 'immobile-source-namespace', $this->getNsText() );
} elseif ( !$this->isMovable() ) {
// Less specific message for rarer cases
- $errors[] = array( 'immobile-page' );
+ $errors[] = array( 'immobile-source-page' );
}
} elseif ( $action == 'move-target' ) {
if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
@@ -1575,6 +1887,12 @@ class Title {
} elseif ( !$this->isMovable() ) {
$errors[] = array( 'immobile-target-page' );
}
+ } elseif ( $action == 'delete' ) {
+ if ( $doExpensiveQueries && $wgDeleteRevisionsLimit
+ && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() )
+ {
+ $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) );
+ }
}
return $errors;
}
@@ -1591,21 +1909,19 @@ class Title {
* @return Array list of errors
*/
private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
- if( !$doExpensiveQueries ) {
+ // Account creation blocks handled at userlogin.
+ // Unblocking handled in SpecialUnblock
+ if( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) {
return $errors;
}
global $wgContLang, $wgLang, $wgEmailConfirmToEdit;
- if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' ) {
+ if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
$errors[] = array( 'confirmedittext' );
}
- if ( in_array( $action, array( 'read', 'createaccount', 'unblock' ) ) ){
- // Edit blocks should not affect reading.
- // Account creation blocks handled at userlogin.
- // Unblocking handled in SpecialUnblock
- } elseif( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ){
+ 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 ) {
@@ -1619,7 +1935,7 @@ class Title {
if ( $reason == '' ) {
$reason = wfMsg( 'blockednoreason' );
}
- $ip = wfGetIP();
+ $ip = $user->getRequest()->getIP();
if ( is_numeric( $id ) ) {
$name = User::whoIs( $id );
@@ -1647,6 +1963,127 @@ class Title {
}
/**
+ * Check that the user is allowed to read this page.
+ *
+ * @param $action String the action to check
+ * @param $user User to check
+ * @param $errors Array list of current errors
+ * @param $doExpensiveQueries Boolean whether or not to perform expensive queries
+ * @param $short Boolean short circuit on first error
+ *
+ * @return Array list of errors
+ */
+ private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
+ global $wgWhitelistRead, $wgGroupPermissions, $wgRevokePermissions;
+ static $useShortcut = null;
+
+ # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
+ if ( is_null( $useShortcut ) ) {
+ $useShortcut = true;
+ if ( empty( $wgGroupPermissions['*']['read'] ) ) {
+ # Not a public wiki, so no shortcut
+ $useShortcut = false;
+ } elseif ( !empty( $wgRevokePermissions ) ) {
+ /**
+ * Iterate through each group with permissions being revoked (key not included since we don't care
+ * what the group name is), then check if the read permission is being revoked. If it is, then
+ * we don't use the shortcut below since the user might not be able to read, even though anon
+ * reading is allowed.
+ */
+ foreach ( $wgRevokePermissions as $perms ) {
+ if ( !empty( $perms['read'] ) ) {
+ # We might be removing the read right from the user, so no shortcut
+ $useShortcut = false;
+ break;
+ }
+ }
+ }
+ }
+
+ $whitelisted = false;
+ if ( $useShortcut ) {
+ # 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 with and without underscores
+ if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
+ # Check for explicit whitelisting
+ $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 !== false ) {
+ $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
+ if ( in_array( $pure, $wgWhitelistRead, true ) ) {
+ $whitelisted = true;
+ }
+ }
+ }
+ }
+
+ 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 $action String the action to check
+ * @param $short Boolean 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)
@@ -1660,20 +2097,28 @@ class Title {
protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) {
wfProfileIn( __METHOD__ );
- $errors = array();
- $checks = array(
- 'checkQuickPermissions',
- 'checkPermissionHooks',
- 'checkSpecialsAndNSPermissions',
- 'checkCSSandJSPermissions',
- 'checkPageRestrictions',
- 'checkCascadingSourcesRestrictions',
- 'checkActionPermissions',
- 'checkUserBlock'
- );
+ # Read has special handling
+ if ( $action == 'read' ) {
+ $checks = array(
+ 'checkPermissionHooks',
+ 'checkReadPermissions',
+ );
+ } else {
+ $checks = array(
+ 'checkQuickPermissions',
+ 'checkPermissionHooks',
+ 'checkSpecialsAndNSPermissions',
+ 'checkCSSandJSPermissions',
+ 'checkPageRestrictions',
+ 'checkCascadingSourcesRestrictions',
+ 'checkActionPermissions',
+ 'checkUserBlock'
+ );
+ }
+ $errors = array();
while( count( $checks ) > 0 &&
- !( $short && count( $errors ) > 0 ) ) {
+ !( $short && count( $errors ) > 0 ) ) {
$method = array_shift( $checks );
$errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
}
@@ -1683,6 +2128,79 @@ class Title {
}
/**
+ * Protect css subpages of user pages: can $wgUser edit
+ * this page?
+ *
+ * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
+ * @return Bool
+ */
+ public function userCanEditCssSubpage() {
+ global $wgUser;
+ wfDeprecated( __METHOD__, '1.19' );
+ return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
+ || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
+ }
+
+ /**
+ * Protect js subpages of user pages: can $wgUser edit
+ * this page?
+ *
+ * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead.
+ * @return Bool
+ */
+ public function userCanEditJsSubpage() {
+ global $wgUser;
+ wfDeprecated( __METHOD__, '1.19' );
+ return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
+ || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
+ }
+
+ /**
+ * 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->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.
*
@@ -1715,66 +2233,24 @@ class Title {
/**
* Update the title protection status
*
+ * @deprecated in 1.19; will be removed in 1.20. Use WikiPage::doUpdateRestrictions() instead.
* @param $create_perm String Permission required for creation
* @param $reason String Reason for protection
* @param $expiry String Expiry timestamp
* @return boolean true
*/
public function updateTitleProtection( $create_perm, $reason, $expiry ) {
- global $wgUser, $wgContLang;
-
- if ( $create_perm == implode( ',', $this->getRestrictions( 'create' ) )
- && $expiry == $this->mRestrictionsExpiry['create'] ) {
- // No change
- return true;
- }
-
- list ( $namespace, $title ) = array( $this->getNamespace(), $this->getDBkey() );
-
- $dbw = wfGetDB( DB_MASTER );
-
- $encodedExpiry = $dbw->encodeExpiry( $expiry );
+ wfDeprecated( __METHOD__, '1.19' );
- $expiry_description = '';
- if ( $encodedExpiry != $dbw->getInfinity() ) {
- $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ),
- $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ) . ')';
- } else {
- $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ) . ')';
- }
-
- # Update protection table
- if ( $create_perm != '' ) {
- $this->mTitleProtection = array(
- 'pt_namespace' => $namespace,
- 'pt_title' => $title,
- 'pt_create_perm' => $create_perm,
- 'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ),
- 'pt_expiry' => $encodedExpiry,
- 'pt_user' => $wgUser->getId(),
- 'pt_reason' => $reason,
- );
- $dbw->replace( 'protected_titles', array( array( 'pt_namespace', 'pt_title' ) ),
- $this->mTitleProtection, __METHOD__ );
- } else {
- $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
- 'pt_title' => $title ), __METHOD__ );
- $this->mTitleProtection = false;
- }
+ global $wgUser;
- # Update the protection log
- if ( $dbw->affectedRows() ) {
- $log = new LogPage( 'protect' );
+ $limit = array( 'create' => $create_perm );
+ $expiry = array( 'create' => $expiry );
- if ( $create_perm ) {
- $params = array( "[create=$create_perm] $expiry_description", '' );
- $log->addEntry( ( isset( $this->mRestrictions['create'] ) && $this->mRestrictions['create'] ) ? 'modify' : 'protect', $this, trim( $reason ), $params );
- } else {
- $log->addEntry( 'unprotect', $this, $reason );
- }
- }
+ $page = WikiPage::factory( $this );
+ $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser );
- return true;
+ return $status->isOK();
}
/**
@@ -1792,284 +2268,81 @@ class Title {
}
/**
- * Would anybody with sufficient privileges be able to move this page?
- * Some pages just aren't movable.
- *
- * @return Bool TRUE or FALSE
- */
- public function isMovable() {
- return MWNamespace::isMovable( $this->getNamespace() ) && $this->getInterwiki() == '';
- }
-
- /**
- * Can $wgUser read this page?
+ * Is this page "semi-protected" - the *only* protection is autoconfirm?
*
+ * @param $action String Action to check (default: edit)
* @return Bool
- * @todo fold these checks into userCan()
*/
- public function userCanRead() {
- global $wgUser, $wgGroupPermissions;
-
- static $useShortcut = null;
-
- # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
- if ( is_null( $useShortcut ) ) {
- global $wgRevokePermissions;
- $useShortcut = true;
- if ( empty( $wgGroupPermissions['*']['read'] ) ) {
- # Not a public wiki, so no shortcut
- $useShortcut = false;
- } elseif ( !empty( $wgRevokePermissions ) ) {
- /**
- * Iterate through each group with permissions being revoked (key not included since we don't care
- * what the group name is), then check if the read permission is being revoked. If it is, then
- * we don't use the shortcut below since the user might not be able to read, even though anon
- * reading is allowed.
- */
- foreach ( $wgRevokePermissions as $perms ) {
- if ( !empty( $perms['read'] ) ) {
- # We might be removing the read right from the user, so no shortcut
- $useShortcut = false;
- break;
+ public function isSemiProtected( $action = 'edit' ) {
+ if ( $this->exists() ) {
+ $restrictions = $this->getRestrictions( $action );
+ if ( count( $restrictions ) > 0 ) {
+ foreach ( $restrictions as $restriction ) {
+ if ( strtolower( $restriction ) != 'autoconfirmed' ) {
+ return false;
}
}
+ } else {
+ # Not protected
+ return false;
}
- }
-
- $result = null;
- wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
- if ( $result !== null ) {
- return $result;
- }
-
- # Shortcut for public wikis, allows skipping quite a bit of code
- if ( $useShortcut ) {
- return true;
- }
-
- if ( $wgUser->isAllowed( 'read' ) ) {
return true;
} else {
- global $wgWhitelistRead;
-
- # Always grant access to the login page.
- # Even anons need to be able to log in.
- if ( $this->isSpecial( 'Userlogin' )
- || $this->isSpecial( 'ChangePassword' )
- || $this->isSpecial( 'PasswordReset' )
- ) {
- return true;
- }
-
- # Bail out if there isn't whitelist
- if ( !is_array( $wgWhitelistRead ) ) {
- return false;
- }
-
- # Check for explicit whitelisting
- $name = $this->getPrefixedText();
- $dbName = $this->getPrefixedDBKey();
- // Check with and without underscores
- if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) )
- return true;
-
- # Old settings might have the title prefixed with
- # a colon for main-namespace pages
- if ( $this->getNamespace() == NS_MAIN ) {
- if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
- return true;
- }
- }
-
- # If it's a special page, ditch the subpage bit and check again
- if ( $this->getNamespace() == NS_SPECIAL ) {
- $name = $this->getDBkey();
- list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
- if ( $name === false ) {
- # Invalid special page, but we show standard login required message
- return false;
- }
-
- $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
- if ( in_array( $pure, $wgWhitelistRead, true ) ) {
- return true;
- }
- }
-
+ # If it doesn't exist, it can't be protected
+ return false;
}
- return false;
}
/**
- * Is this the mainpage?
- * @note Title::newFromText seams to be sufficiently optimized by the title
- * cache that we don't need to over-optimize by doing direct comparisons and
- * acidentally creating new bugs where $title->equals( Title::newFromText() )
- * ends up reporting something differently than $title->isMainPage();
- *
- * @return Bool
- */
- public function isMainPage() {
- return $this->equals( Title::newMainPage() );
- }
-
- /**
- * Is this a talk page of some sort?
- *
- * @return Bool
- */
- public function isTalkPage() {
- return MWNamespace::isTalk( $this->getNamespace() );
- }
-
- /**
- * Is this a subpage?
- *
- * @return Bool
- */
- public function isSubpage() {
- return MWNamespace::hasSubpages( $this->mNamespace )
- ? strpos( $this->getText(), '/' ) !== false
- : false;
- }
-
- /**
- * Does this have subpages? (Warning, usually requires an extra DB query.)
+ * Does the title correspond to a protected article?
*
+ * @param $action String the action the page is protected from,
+ * by default checks all actions.
* @return Bool
*/
- public function hasSubpages() {
- if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
- # Duh
- return false;
- }
-
- # We dynamically add a member variable for the purpose of this method
- # alone to cache the result. There's no point in having it hanging
- # around uninitialized in every Title object; therefore we only add it
- # if needed and don't declare it statically.
- if ( isset( $this->mHasSubpages ) ) {
- return $this->mHasSubpages;
- }
+ public function isProtected( $action = '' ) {
+ global $wgRestrictionLevels;
- $subpages = $this->getSubpages( 1 );
- if ( $subpages instanceof TitleArray ) {
- return $this->mHasSubpages = (bool)$subpages->count();
- }
- return $this->mHasSubpages = false;
- }
+ $restrictionTypes = $this->getRestrictionTypes();
- /**
- * Get all subpages of this page.
- *
- * @param $limit Int maximum number of subpages to fetch; -1 for no limit
- * @return mixed TitleArray, or empty array if this page's namespace
- * doesn't allow subpages
- */
- public function getSubpages( $limit = -1 ) {
- if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
- return array();
+ # Special pages have inherent protection
+ if( $this->isSpecialPage() ) {
+ return true;
}
- $dbr = wfGetDB( DB_SLAVE );
- $conds['page_namespace'] = $this->getNamespace();
- $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
- $options = array();
- if ( $limit > -1 ) {
- $options['LIMIT'] = $limit;
+ # 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 $this->mSubpages = TitleArray::newFromResult(
- $dbr->select( 'page',
- array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
- $conds,
- __METHOD__,
- $options
- )
- );
- }
-
- /**
- * Could this page contain custom CSS or JavaScript, based
- * on the title?
- *
- * @return Bool
- */
- public function isCssOrJsPage() {
- return $this->mNamespace == NS_MEDIAWIKI
- && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
- }
-
- /**
- * Is this a .css or .js subpage of a user page?
- * @return Bool
- */
- public function isCssJsSubpage() {
- return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
- }
-
- /**
- * Is this a *valid* .css or .js subpage of a user page?
- *
- * @return Bool
- * @deprecated since 1.17
- */
- public function isValidCssJsSubpage() {
- return $this->isCssJsSubpage();
- }
-
- /**
- * Trim down a .css or .js subpage title to get the corresponding skin name
- *
- * @return string containing skin name from .css or .js subpage title
- */
- public function getSkinFromCssJsSubpage() {
- $subpage = explode( '/', $this->mTextform );
- $subpage = $subpage[ count( $subpage ) - 1 ];
- return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) );
- }
-
- /**
- * Is this a .css subpage of a user page?
- *
- * @return Bool
- */
- public function isCssSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
- }
- /**
- * Is this a .js subpage of a user page?
- *
- * @return Bool
- */
- public function isJsSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+ return false;
}
/**
- * Protect css subpages of user pages: can $wgUser edit
- * this page?
+ * Determines if $user is unable to edit this page because it has been protected
+ * by $wgNamespaceProtection.
*
+ * @param $user User object to check permissions
* @return Bool
- * @todo XXX: this might be better using restrictions
*/
- public function userCanEditCssSubpage() {
- global $wgUser;
- return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) )
- || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
- }
+ public function isNamespaceProtected( User $user ) {
+ global $wgNamespaceProtection;
- /**
- * Protect js subpages of user pages: can $wgUser edit
- * this page?
- *
- * @return Bool
- * @todo XXX: this might be better using restrictions
- */
- public function userCanEditJsSubpage() {
- global $wgUser;
- return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) )
- || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) );
+ if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
+ foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
+ if ( $right != '' && !$user->isAllowed( $right ) ) {
+ return true;
+ }
+ }
+ }
+ return false;
}
/**
@@ -2181,6 +2454,34 @@ class Title {
}
/**
+ * Accessor/initialisation for mRestrictions
+ *
+ * @param $action String action that permission needs to be checked for
+ * @return Array of Strings the array of groups allowed to edit this article
+ */
+ public function getRestrictions( $action ) {
+ if ( !$this->mRestrictionsLoaded ) {
+ $this->loadRestrictions();
+ }
+ return isset( $this->mRestrictions[$action] )
+ ? $this->mRestrictions[$action]
+ : array();
+ }
+
+ /**
+ * Get the expiry time for the restriction against a given 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 Boolean
@@ -2336,6 +2637,15 @@ class Title {
}
/**
+ * 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
*/
static function purgeExpiredRestrictions() {
@@ -2354,31 +2664,58 @@ class Title {
}
/**
- * Accessor/initialisation for mRestrictions
+ * Does this have subpages? (Warning, usually requires an extra DB query.)
*
- * @param $action String action that permission needs to be checked for
- * @return Array of Strings the array of groups allowed to edit this article
+ * @return Bool
*/
- public function getRestrictions( $action ) {
- if ( !$this->mRestrictionsLoaded ) {
- $this->loadRestrictions();
+ public function hasSubpages() {
+ if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+ # Duh
+ return false;
}
- return isset( $this->mRestrictions[$action] )
- ? $this->mRestrictions[$action]
- : array();
+
+ # We dynamically add a member variable for the purpose of this method
+ # alone to cache the result. There's no point in having it hanging
+ # around uninitialized in every Title object; therefore we only add it
+ # if needed and don't declare it statically.
+ if ( isset( $this->mHasSubpages ) ) {
+ return $this->mHasSubpages;
+ }
+
+ $subpages = $this->getSubpages( 1 );
+ if ( $subpages instanceof TitleArray ) {
+ return $this->mHasSubpages = (bool)$subpages->count();
+ }
+ return $this->mHasSubpages = false;
}
/**
- * Get the expiry time for the restriction against a given action
+ * Get all subpages of this page.
*
- * @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.
+ * @param $limit Int maximum number of subpages to fetch; -1 for no limit
+ * @return mixed TitleArray, or empty array if this page's namespace
+ * doesn't allow subpages
*/
- public function getRestrictionExpiry( $action ) {
- if ( !$this->mRestrictionsLoaded ) {
- $this->loadRestrictions();
+ public function getSubpages( $limit = -1 ) {
+ if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
+ return array();
}
- return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $conds['page_namespace'] = $this->getNamespace();
+ $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
+ $options = array();
+ if ( $limit > -1 ) {
+ $options['LIMIT'] = $limit;
+ }
+ return $this->mSubpages = TitleArray::newFromResult(
+ $dbr->select( 'page',
+ array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
+ $conds,
+ __METHOD__,
+ $options
+ )
+ );
}
/**
@@ -2521,15 +2858,15 @@ class Title {
* This clears some fields in this object, and clears any associated
* keys in the "bad links" section of the link cache.
*
- * - This is called from Article::doEdit() and Article::insertOn() to allow
+ * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
* loading of the new page_id. It's also called from
- * Article::doDeleteArticle()
+ * WikiPage::doDeleteArticle()
*
* @param $newid Int the new Article ID
*/
public function resetArticleID( $newid ) {
$linkCache = LinkCache::singleton();
- $linkCache->clearBadLink( $this->getPrefixedDBkey() );
+ $linkCache->clearLink( $this );
if ( $newid === false ) {
$this->mArticleID = -1;
@@ -2541,73 +2878,7 @@ class Title {
$this->mRedirect = null;
$this->mLength = -1;
$this->mLatestID = false;
- }
-
- /**
- * Updates page_touched for this page; called from LinksUpdate.php
- *
- * @return Bool true if the update succeded
- */
- public function invalidateCache() {
- if ( wfReadOnly() ) {
- return;
- }
- $dbw = wfGetDB( DB_MASTER );
- $success = $dbw->update(
- 'page',
- array( 'page_touched' => $dbw->timestamp() ),
- $this->pageCond(),
- __METHOD__
- );
- HTMLFileCache::clearFileCache( $this );
- return $success;
- }
-
- /**
- * Prefix some arbitrary text with the namespace or interwiki prefix
- * of this object
- *
- * @param $name String the text
- * @return String the prefixed text
- * @private
- */
- private function prefix( $name ) {
- $p = '';
- if ( $this->mInterwiki != '' ) {
- $p = $this->mInterwiki . ':';
- }
-
- if ( 0 != $this->mNamespace ) {
- $p .= $this->getNsText() . ':';
- }
- return $p . $name;
- }
-
- /**
- * Returns a simple regex that will match on characters and sequences invalid in titles.
- * Note that this doesn't pick up many things that could be wrong with titles, but that
- * replacing this regex with something valid will make many titles valid.
- *
- * @return String regex string
- */
- static function getTitleInvalidRegex() {
- static $rxTc = false;
- if ( !$rxTc ) {
- # Matching titles will be held as illegal.
- $rxTc = '/' .
- # Any character not allowed is forbidden...
- '[^' . Title::legalChars() . ']' .
- # URL percent encoding sequences interfere with the ability
- # to round-trip titles -- you can't link to them consistently.
- '|%[0-9A-Fa-f]{2}' .
- # XML/HTML character references produce similar issues.
- '|&[A-Za-z0-9\x80-\xff]+;' .
- '|&#[0-9]+;' .
- '|&#x[0-9A-Fa-f]+;' .
- '/S';
- }
-
- return $rxTc;
+ $this->mEstimateRevisions = null;
}
/**
@@ -2811,7 +3082,7 @@ class Title {
: $dbkey;
// Any remaining initial :s are illegal.
- if ( $dbkey !== '' && ':' == $dbkey { 0 } ) {
+ if ( $dbkey !== '' && ':' == $dbkey[0] ) {
return false;
}
@@ -2825,44 +3096,6 @@ class Title {
}
/**
- * Set the fragment for this title. Removes the first character from the
- * specified fragment before setting, so it assumes you're passing it with
- * an initial "#".
- *
- * Deprecated for public use, use Title::makeTitle() with fragment parameter.
- * Still in active use privately.
- *
- * @param $fragment String text
- */
- public function setFragment( $fragment ) {
- $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
- }
-
- /**
- * Get a Title object associated with the talk page of this article
- *
- * @return Title the object for the talk page
- */
- public function getTalkPage() {
- return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
- }
-
- /**
- * Get a title object associated with the subject page of this
- * talk page
- *
- * @return Title the object for the subject page
- */
- public function getSubjectPage() {
- // Is this the same title?
- $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
- if ( $this->getNamespace() == $subjectNS ) {
- return $this;
- }
- return Title::makeTitle( $subjectNS, $this->getDBkey() );
- }
-
- /**
* Get an array of Title objects linking to this Title
* Also stores the IDs in the link cache.
*
@@ -2875,8 +3108,6 @@ class Title {
* @return Array of Title objects linking here
*/
public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
- $linkCache = LinkCache::singleton();
-
if ( count( $options ) > 0 ) {
$db = wfGetDB( DB_MASTER );
} else {
@@ -2895,11 +3126,12 @@ class Title {
);
$retVal = array();
- if ( $db->numRows( $res ) ) {
+ if ( $res->numRows() ) {
+ $linkCache = LinkCache::singleton();
foreach ( $res as $row ) {
$titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
if ( $titleObj ) {
- $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect, $row->page_latest );
+ $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
$retVal[] = $titleObj;
}
}
@@ -2922,6 +3154,76 @@ class Title {
}
/**
+ * Get an array of Title objects linked from this Title
+ * Also stores the IDs in the link cache.
+ *
+ * WARNING: do not use this function on arbitrary user-supplied titles!
+ * On heavily-used templates it will max out the memory.
+ *
+ * @param $options Array: may be FOR UPDATE
+ * @param $table String: table name
+ * @param $prefix String: fields prefix
+ * @return Array of Title objects linking here
+ */
+ public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+ $id = $this->getArticleId();
+
+ # If the page doesn't exist; there can't be any link from this page
+ if ( !$id ) {
+ return array();
+ }
+
+ if ( count( $options ) > 0 ) {
+ $db = wfGetDB( DB_MASTER );
+ } else {
+ $db = wfGetDB( DB_SLAVE );
+ }
+
+ $namespaceFiled = "{$prefix}_namespace";
+ $titleField = "{$prefix}_title";
+
+ $res = $db->select(
+ array( $table, 'page' ),
+ array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+ array( "{$prefix}_from" => $id ),
+ __METHOD__,
+ $options,
+ array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) )
+ );
+
+ $retVal = array();
+ if ( $res->numRows() ) {
+ $linkCache = LinkCache::singleton();
+ foreach ( $res as $row ) {
+ $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField );
+ if ( $titleObj ) {
+ if ( $row->page_id ) {
+ $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
+ } else {
+ $linkCache->addBadLinkObj( $titleObj );
+ }
+ $retVal[] = $titleObj;
+ }
+ }
+ }
+ return $retVal;
+ }
+
+ /**
+ * Get an array of Title objects used on this Title as a template
+ * Also stores the IDs in the link cache.
+ *
+ * WARNING: do not use this function on arbitrary user-supplied titles!
+ * On heavily-used templates it will max out the memory.
+ *
+ * @param $options Array: may be FOR UPDATE
+ * @return Array of Title the Title objects used here
+ */
+ public function getTemplateLinksFrom( $options = array() ) {
+ return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
+ }
+
+ /**
* Get an array of Title objects referring to non-existent articles linked from this page
*
* @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case)
@@ -3109,7 +3411,7 @@ class Title {
$errors = array();
// wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below
-
+
$file = wfLocalFile( $this );
if ( $file->exists() ) {
if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
@@ -3119,14 +3421,14 @@ class Title {
$errors[] = array( 'imagetypemismatch' );
}
}
-
+
if ( $nt->getNamespace() != NS_FILE ) {
$errors[] = array( 'imagenocrossnamespace' );
- // From here we want to do checks on a file object, so if we can't
+ // From here we want to do checks on a file object, so if we can't
// create one, we must return.
return $errors;
}
-
+
// wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here
$destFile = wfLocalFile( $nt );
@@ -3157,8 +3459,8 @@ class Title {
return $err;
}
- // If it is a file, move it first. It is done before all other moving stuff is
- // done because it's hard to revert
+ // If it is a file, move it first.
+ // It is done before all other moving stuff is done because it's hard to revert.
$dbw = wfGetDB( DB_MASTER );
if ( $this->getNamespace() == NS_FILE ) {
$file = wfLocalFile( $this );
@@ -3168,12 +3470,14 @@ class Title {
return $status->getErrorsArray();
}
}
+ // Clear RepoGroup process cache
+ RepoGroup::singleton()->clearCache( $this );
+ RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
}
$dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own.
$pageid = $this->getArticleID( self::GAID_FOR_UPDATE );
$protected = $this->isProtected();
- $pageCountChange = ( $createRedirect ? 1 : 0 ) - ( $nt->exists() ? 1 : 0 );
// Do the actual move
$err = $this->moveToInternal( $nt, $reason, $createRedirect );
@@ -3183,8 +3487,6 @@ class Title {
return $err;
}
- $redirid = $this->getArticleID();
-
// Refresh the sortkey for this row. Be careful to avoid resetting
// cl_timestamp, which may disturb time-based lists on some sites.
$prefixes = $dbw->select(
@@ -3208,6 +3510,8 @@ class Title {
);
}
+ $redirid = $this->getArticleID();
+
if ( $protected ) {
# Protect the redirect title as the title used to be...
$dbw->insertSelect( 'page_restrictions', 'page_restrictions',
@@ -3243,51 +3547,8 @@ class Title {
WatchedItem::duplicateEntries( $this, $nt );
}
- # Update search engine
- $u = new SearchUpdate( $pageid, $nt->getPrefixedDBkey() );
- $u->doUpdate();
- $u = new SearchUpdate( $redirid, $this->getPrefixedDBkey(), '' );
- $u->doUpdate();
-
$dbw->commit();
- # Update site_stats
- if ( $this->isContentPage() && !$nt->isContentPage() ) {
- # No longer a content page
- # Not viewed, edited, removing
- $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange );
- } elseif ( !$this->isContentPage() && $nt->isContentPage() ) {
- # Now a content page
- # Not viewed, edited, adding
- $u = new SiteStatsUpdate( 0, 1, + 1, $pageCountChange );
- } elseif ( $pageCountChange ) {
- # Redirect added
- $u = new SiteStatsUpdate( 0, 0, 0, 1 );
- } else {
- # Nothing special
- $u = false;
- }
- if ( $u ) {
- $u->doUpdate();
- }
-
- # Update message cache for interface messages
- if ( $this->getNamespace() == NS_MEDIAWIKI ) {
- # @bug 17860: old article can be deleted, if this the case,
- # delete it from message cache
- if ( $this->getArticleID() === 0 ) {
- MessageCache::singleton()->replace( $this->getDBkey(), false );
- } else {
- $oldarticle = new Article( $this );
- MessageCache::singleton()->replace( $this->getDBkey(), $oldarticle->getContent() );
- }
- }
- if ( $nt->getNamespace() == NS_MEDIAWIKI ) {
- $newarticle = new Article( $nt );
- MessageCache::singleton()->replace( $nt->getDBkey(), $newarticle->getContent() );
- }
-
- global $wgUser;
wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) );
return true;
}
@@ -3304,11 +3565,28 @@ class Title {
private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
global $wgUser, $wgContLang;
- $moveOverRedirect = $nt->exists();
+ if ( $nt->exists() ) {
+ $moveOverRedirect = true;
+ $logType = 'move_redir';
+ } else {
+ $moveOverRedirect = false;
+ $logType = 'move';
+ }
+
+ $redirectSuppressed = !$createRedirect && $wgUser->isAllowed( 'suppressredirect' );
- $commentMsg = ( $moveOverRedirect ? '1movedto2_redir' : '1movedto2' );
- $comment = wfMsgForContent( $commentMsg, $this->getPrefixedText(), $nt->getPrefixedText() );
+ $logEntry = new ManualLogEntry( 'move', $logType );
+ $logEntry->setPerformer( $wgUser );
+ $logEntry->setTarget( $this );
+ $logEntry->setComment( $reason );
+ $logEntry->setParameters( array(
+ '4::target' => $nt->getPrefixedText(),
+ '5::noredir' => $redirectSuppressed ? '1': '0',
+ ) );
+ $formatter = LogFormatter::newFromEntry( $logEntry );
+ $formatter->setContext( RequestContext::newExtraneousContext( $this ) );
+ $comment = $formatter->getPlainActionText();
if ( $reason ) {
$comment .= wfMsgForContent( 'colon-separator' ) . $reason;
}
@@ -3320,38 +3598,18 @@ class Title {
$dbw = wfGetDB( DB_MASTER );
- if ( $moveOverRedirect ) {
- $rcts = $dbw->timestamp( $nt->getEarliestRevTime() );
+ $newpage = WikiPage::factory( $nt );
+ if ( $moveOverRedirect ) {
$newid = $nt->getArticleID();
- $newns = $nt->getNamespace();
- $newdbk = $nt->getDBkey();
# Delete the old redirect. We don't save it to history since
# by definition if we've got here it's rather uninteresting.
# We have to remove it so that the next step doesn't trigger
# a conflict on the unique namespace+title index...
$dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
- if ( !$dbw->cascadingDeletes() ) {
- $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ );
- global $wgUseTrackbacks;
- if ( $wgUseTrackbacks ) {
- $dbw->delete( 'trackbacks', array( 'tb_page' => $newid ), __METHOD__ );
- }
- $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
- $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ );
- $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ );
- $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ );
- $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ );
- $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ );
- $dbw->delete( 'iwlinks', array( 'iwl_from' => $newid ), __METHOD__ );
- $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ );
- }
- // If the target page was recently created, it may have an entry in recentchanges still
- $dbw->delete( 'recentchanges',
- array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ),
- __METHOD__
- );
+
+ $newpage->doDeleteUpdates( $newid );
}
# Save a null revision in the page's history notifying of the move
@@ -3361,68 +3619,56 @@ class Title {
}
$nullRevId = $nullRevision->insertOn( $dbw );
- $now = wfTimestampNow();
# Change the name of the target page:
$dbw->update( 'page',
/* SET */ array(
- 'page_touched' => $dbw->timestamp( $now ),
'page_namespace' => $nt->getNamespace(),
'page_title' => $nt->getDBkey(),
- 'page_latest' => $nullRevId,
),
/* WHERE */ array( 'page_id' => $oldid ),
__METHOD__
);
+
+ $this->resetArticleID( 0 );
$nt->resetArticleID( $oldid );
- $article = new Article( $nt );
- wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $wgUser ) );
- $article->setCachedLastEditTime( $now );
+ $newpage->updateRevisionOn( $dbw, $nullRevision );
+
+ wfRunHooks( 'NewRevisionFromEditComplete',
+ array( $newpage, $nullRevision, $latest, $wgUser ) );
+
+ $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) );
+
+ if ( !$moveOverRedirect ) {
+ WikiPage::onArticleCreate( $nt );
+ }
# Recreate the redirect, this time in the other direction.
- if ( $createRedirect || !$wgUser->isAllowed( 'suppressredirect' ) ) {
+ if ( $redirectSuppressed ) {
+ WikiPage::onArticleDelete( $this );
+ } else {
$mwRedir = MagicWord::get( 'redirect' );
$redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
- $redirectArticle = new Article( $this );
+ $redirectArticle = WikiPage::factory( $this );
$newid = $redirectArticle->insertOn( $dbw );
- $redirectRevision = new Revision( array(
- 'page' => $newid,
- 'comment' => $comment,
- 'text' => $redirectText ) );
- $redirectRevision->insertOn( $dbw );
- $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
-
- wfRunHooks( 'NewRevisionFromEditComplete', array( $redirectArticle, $redirectRevision, false, $wgUser ) );
-
- # Now, we record the link from the redirect to the new title.
- # It should have no other outgoing links...
- $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
- $dbw->insert( 'pagelinks',
- array(
- 'pl_from' => $newid,
- 'pl_namespace' => $nt->getNamespace(),
- 'pl_title' => $nt->getDBkey() ),
- __METHOD__ );
- $redirectSuppressed = false;
- } else {
- $this->resetArticleID( 0 );
- $redirectSuppressed = true;
+ if ( $newid ) { // sanity
+ $redirectRevision = new Revision( array(
+ 'page' => $newid,
+ 'comment' => $comment,
+ 'text' => $redirectText ) );
+ $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+
+ wfRunHooks( 'NewRevisionFromEditComplete',
+ array( $redirectArticle, $redirectRevision, false, $wgUser ) );
+
+ $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) );
+ }
}
# Log the move
- $log = new LogPage( 'move' );
- $logType = ( $moveOverRedirect ? 'move_redir' : 'move' );
- $log->addEntry( $logType, $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
-
- # Purge caches for old and new titles
- if ( $moveOverRedirect ) {
- # A simple purge is enough when moving over a redirect
- $nt->purgeSquid();
- } else {
- # Purge caches as per article creation, including any pages that link to this title
- Article::onArticleCreate( $nt );
- }
- $this->purgeSquid();
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
}
/**
@@ -3558,6 +3804,9 @@ class Title {
}
# Get the article text
$rev = Revision::newFromTitle( $nt );
+ if( !is_object( $rev ) ){
+ return false;
+ }
$text = $rev->getText();
# Does the redirect point to the source?
# Or is it a broken self-redirect, usually caused by namespace collisions?
@@ -3579,15 +3828,6 @@ class Title {
}
/**
- * Can this title be added to a user's watchlist?
- *
- * @return Bool TRUE or FALSE
- */
- public function isWatchable() {
- return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
- }
-
- /**
* Get categories to which this Title belongs and return an array of
* categories' names.
*
@@ -3748,6 +3988,41 @@ class Title {
}
/**
+ * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
+ *
+ * @return bool
+ */
+ public function isBigDeletion() {
+ global $wgDeleteRevisionsLimit;
+
+ if ( !$wgDeleteRevisionsLimit ) {
+ return false;
+ }
+
+ $revCount = $this->estimateRevisionCount();
+ return $revCount > $wgDeleteRevisionsLimit;
+ }
+
+ /**
+ * Get the approximate revision count of this page.
+ *
+ * @return int
+ */
+ public function estimateRevisionCount() {
+ if ( !$this->exists() ) {
+ return 0;
+ }
+
+ if ( $this->mEstimateRevisions === null ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
+ array( 'rev_page' => $this->getArticleId() ), __METHOD__ );
+ }
+
+ return $this->mEstimateRevisions;
+ }
+
+ /**
* Get the number of revisions between the given revision.
* Used for diffs and other things that really need it.
*
@@ -3821,28 +4096,15 @@ class Title {
}
/**
- * Callback for usort() to do title sorts by (namespace, title)
- *
- * @param $a Title
- * @param $b Title
- *
- * @return Integer: result of string comparison, or namespace comparison
- */
- public static function compare( $a, $b ) {
- if ( $a->getNamespace() == $b->getNamespace() ) {
- return strcmp( $a->getText(), $b->getText() );
- } else {
- return $a->getNamespace() - $b->getNamespace();
- }
- }
-
- /**
- * Return a string representation of this title
+ * Check if this title is a subpage of another title
*
- * @return String representation of this title
+ * @param $title Title
+ * @return Bool
*/
- public function __toString() {
- return $this->getPrefixedText();
+ public function isSubpageOf( Title $title ) {
+ return $this->getInterwiki() === $title->getInterwiki()
+ && $this->getNamespace() == $title->getNamespace()
+ && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
}
/**
@@ -3956,13 +4218,23 @@ class Title {
}
/**
- * Is this in a namespace that allows actual pages?
+ * Updates page_touched for this page; called from LinksUpdate.php
*
- * @return Bool
- * @internal note -- uses hardcoded namespace index instead of constants
+ * @return Bool true if the update succeded
*/
- public function canExist() {
- return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA;
+ public function invalidateCache() {
+ if ( wfReadOnly() ) {
+ return false;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $success = $dbw->update(
+ 'page',
+ array( 'page_touched' => $dbw->timestamp() ),
+ $this->pageCond(),
+ __METHOD__
+ );
+ HTMLFileCache::clearFileCache( $this );
+ return $success;
}
/**
@@ -4030,46 +4302,6 @@ class Title {
}
/**
- * Get the trackback URL for this page
- *
- * @return String Trackback URL
- */
- public function trackbackURL() {
- global $wgScriptPath, $wgServer, $wgScriptExtension;
-
- return "$wgServer$wgScriptPath/trackback$wgScriptExtension?article="
- . htmlspecialchars( urlencode( $this->getPrefixedDBkey() ) );
- }
-
- /**
- * Get the trackback RDF for this page
- *
- * @return String Trackback RDF
- */
- public function trackbackRDF() {
- $url = htmlspecialchars( $this->getFullURL() );
- $title = htmlspecialchars( $this->getText() );
- $tburl = $this->trackbackURL();
-
- // Autodiscovery RDF is placed in comments so HTML validator
- // won't barf. This is a rather icky workaround, but seems
- // frequently used by this kind of RDF thingy.
- //
- // Spec: http://www.sixapart.com/pronet/docs/trackback_spec
- return "<!--
-<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"
- xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
- xmlns:trackback=\"http://madskills.com/public/xml/rss/module/trackback/\">
-<rdf:Description
- rdf:about=\"$url\"
- dc:identifier=\"$url\"
- dc:title=\"$title\"
- trackback:ping=\"$tburl\" />
-</rdf:RDF>
--->";
- }
-
- /**
* Generate strings used for xml 'id' names in monobook tabs
*
* @param $prepend string defaults to 'nstab-'
@@ -4101,61 +4333,6 @@ class Title {
}
/**
- * Returns true if this is a special page.
- *
- * @return boolean
- */
- public function isSpecialPage() {
- return $this->getNamespace() == NS_SPECIAL;
- }
-
- /**
- * Returns true if this title resolves to the named special page
- *
- * @param $name String The special page name
- * @return boolean
- */
- public function isSpecial( $name ) {
- if ( $this->getNamespace() == NS_SPECIAL ) {
- list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
- if ( $name == $thisName ) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * If the Title refers to a special page alias which is not the local default, resolve
- * the alias, and localise the name as necessary. Otherwise, return $this
- *
- * @return Title
- */
- public function fixSpecialName() {
- if ( $this->getNamespace() == NS_SPECIAL ) {
- list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
- if ( $canonicalName ) {
- $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
- if ( $localName != $this->mDbkeyform ) {
- return Title::makeTitle( NS_SPECIAL, $localName );
- }
- }
- }
- return $this;
- }
-
- /**
- * Is this Title in a namespace which contains content?
- * In other words, is this a content page, for the purposes of calculating
- * statistics, etc?
- *
- * @return Boolean
- */
- public function isContentPage() {
- return MWNamespace::isContent( $this->getNamespace() );
- }
-
- /**
* Get all extant redirects to this Title
*
* @param $ns Int|Null Single namespace to consider; NULL to consider all namespaces
@@ -4212,7 +4389,7 @@ class Title {
/**
* Get a backlink cache object
*
- * @return object BacklinkCache
+ * @return BacklinkCache
*/
function getBacklinkCache() {
if ( is_null( $this->mBacklinkCache ) ) {
@@ -4238,50 +4415,6 @@ class Title {
}
/**
- * Returns restriction types for the current Title
- *
- * @return array applicable restriction types
- */
- public function getRestrictionTypes() {
- if ( $this->getNamespace() == NS_SPECIAL ) {
- 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 restriction types for ' .
- $this->getPrefixedText() . ' are ' . implode( ',', $types ) . "\n" );
-
- return $types;
- }
- /**
- * 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 the raw sort key to be used for categories, with the specified
* prefix. This will be fed to Collation::getSortKey() to get a
* binary sortkey that can be used for actual sorting.
@@ -4320,12 +4453,9 @@ class Title {
*/
public function getPageLanguage() {
global $wgLang;
- if ( $this->getNamespace() == NS_SPECIAL ) {
+ if ( $this->isSpecialPage() ) {
// special pages are in the user language
return $wgLang;
- } elseif ( $this->isRedirect() ) {
- // the arrow on a redirect page is aligned according to the user language
- return $wgLang;
} elseif ( $this->isCssOrJsPage() ) {
// css/js should always be LTR and is, in fact, English
return wfGetLangObj( 'en' );