diff options
Diffstat (limited to 'includes/Title.php')
-rw-r--r-- | includes/Title.php | 2238 |
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' ); |