X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FTitle.php;h=0ef4cda26b1b689b5fd833a27f01c8e823e21fd5;hb=68cad93a447be3e7824c79c0663967ee727a6244;hp=b31b87eceb426f74f56d56d7496eab112d953f6f;hpb=c3e3cc1cd3c6901e4a8d6bbead67beedd63797e4;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Title.php b/includes/Title.php index b31b87eceb..0ef4cda26b 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -63,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? @@ -259,8 +260,7 @@ class Title { * Load Title object fields from a DB row. * If false is given, the title will be treated as non-existing. * - * @param $row Object|false database row - * @return void + * @param $row Object|bool database row */ public function loadFromRow( $row ) { if ( $row ) { // page found @@ -387,8 +387,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; } @@ -442,10 +442,6 @@ class Title { return null; } -# ---------------------------------------------------------------------------- -# Static functions -# ---------------------------------------------------------------------------- - /** * Get the prefixed DB key associated with an ID * @@ -479,6 +475,33 @@ class Title { return $wgLegalTitleChars; } + /** + * 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 @@ -530,6 +553,36 @@ class Title { return $name; } + /** + * 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. @@ -544,6 +597,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. @@ -571,52 +642,50 @@ class Title { return Interwiki::fetch( $this->mInterwiki )->getWikiID(); } - /** - * 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 @@ -655,15 +724,6 @@ class Title { return $wgContLang->getNsText( $this->mNamespace ); } - /** - * 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 * @@ -694,460 +754,751 @@ class Title { } /** - * Get the interwiki prefix (or null string) - * - * @return String Interwiki prefix - */ - public function getInterwiki() { return $this->mInterwiki; } - - /** - * Get the Title fragment (i.e.\ the bit after the #) in text form + * Is this in a namespace that allows actual pages? * - * @return String Title fragment - */ - public function getFragment() { return $this->mFragment; } - - /** - * Get the fragment in URL form, including the "#" character if there is one - * @return String Fragment in URL form + * @return Bool + * @internal note -- uses hardcoded namespace index instead of constants */ - public function getFragmentForURL() { - if ( $this->mFragment == '' ) { - return ''; - } else { - return '#' . Title::escapeFragmentForURL( $this->mFragment ); - } + public function canExist() { + return $this->mNamespace >= NS_MAIN; } /** - * Get the default namespace index, for when there is no namespace + * Can this title be added to a user's watchlist? * - * @return Int Default namespace index + * @return Bool TRUE or FALSE */ - public function getDefaultNamespace() { return $this->mDefaultNamespace; } + public function isWatchable() { + return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); + } /** - * Get title for search index + * Returns true if this is a special page. * - * @return String a stripped-down title string ready for the - * search index + * @return boolean */ - public function getIndexTitle() { - return Title::indexTitle( $this->mNamespace, $this->mTextform ); + public function isSpecialPage() { + return $this->getNamespace() == NS_SPECIAL; } /** - * Get the prefixed database key form + * Returns true if this title resolves to the named special page * - * @return String the prefixed title, with underscores and - * any interwiki and namespace prefixes + * @param $name String The special page name + * @return boolean */ - public function getPrefixedDBkey() { - $s = $this->prefix( $this->mDbkeyform ); - $s = str_replace( ' ', '_', $s ); - return $s; + public function isSpecial( $name ) { + if ( $this->isSpecialPage() ) { + list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); + if ( $name == $thisName ) { + return true; + } + } + return false; } /** - * Get the prefixed title with spaces. - * This is the form usually used for display + * 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 String the prefixed title, with spaces + * @return Title */ - public function getPrefixedText() { - // @todo FIXME: Bad usage of empty() ? - if ( empty( $this->mPrefixedText ) ) { - $s = $this->prefix( $this->mTextform ); - $s = str_replace( '_', ' ', $s ); - $this->mPrefixedText = $s; + 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->mPrefixedText; + return $this; } /** - /** - * Get the prefixed title with spaces, plus any fragment - * (part beginning with '#') - * - * @return String the prefixed title, with spaces and the fragment, including '#' + * 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 getFullText() { - $text = $this->getPrefixedText(); - if ( $this->mFragment != '' ) { - $text .= '#' . $this->mFragment; - } - return $text; + public function inNamespace( $ns ) { + return MWNamespace::equals( $this->getNamespace(), $ns ); } /** - * Get the base page name, i.e. the leftmost part before any slashes + * Returns true if the title is inside one of the specified namespaces. * - * @return String Base name + * @param ...$namespaces The namespaces to check for + * @return bool + * @since 1.19 */ - public function getBaseText() { - if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { - return $this->getText(); + public function inNamespaces( /* ... */ ) { + $namespaces = func_get_args(); + if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { + $namespaces = $namespaces[0]; } - $parts = explode( '/', $this->getText() ); - # Don't discard the real title if there's no subpage involved - if ( count( $parts ) > 1 ) { - unset( $parts[count( $parts ) - 1] ); + foreach ( $namespaces as $ns ) { + if ( $this->inNamespace( $ns ) ) { + return true; + } } - return implode( '/', $parts ); + + return false; } /** - * Get the lowest-level subpage name, i.e. the rightmost part after any slashes + * 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. * - * @return String Subpage name + * 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 + * @return bool */ - public function getSubpageText() { - if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { - return( $this->mTextform ); - } - $parts = explode( '/', $this->mTextform ); - return( $parts[count( $parts ) - 1] ); + public function hasSubjectNamespace( $ns ) { + return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); } /** - * Get a URL-encoded form of the subpage text + * 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 String URL-encoded subpage name + * @return Boolean */ - public function getSubpageUrlForm() { - $text = $this->getSubpageText(); - $text = wfUrlencode( str_replace( ' ', '_', $text ) ); - return( $text ); + public function isContentPage() { + return MWNamespace::isContent( $this->getNamespace() ); } /** - * Get a URL-encoded title (not an actual URL) including interwiki + * Would anybody with sufficient privileges be able to move this page? + * Some pages just aren't movable. * - * @return String the URL-encoded form + * @return Bool TRUE or FALSE */ - public function getPrefixedURL() { - $s = $this->prefix( $this->mDbkeyform ); - $s = wfUrlencode( str_replace( ' ', '_', $s ) ); - return $s; + 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; } /** - * Get a real URL referring to this title, with interwiki link and - * fragment + * 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(); * - * @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.18 + * @return Bool */ - public function getFullURL( $query = '', $variant = false ) { - global $wgServer, $wgRequest; - - # Hand off all the decisions on urls to getLocalURL - $url = $this->getLocalURL( $query, $variant ); - - # 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 ) ); - return $url; + public function isMainPage() { + return $this->equals( Title::newMainPage() ); } /** - * Get a URL with no fragment or server name. If this page is generated - * with action=render, $wgServer is prepended. + * Is this a subpage? * - * @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..) - * @return String the URL + * @return Bool */ - public function getLocalURL( $query = '', $variant = false ) { - global $wgArticlePath, $wgScript, $wgServer, $wgRequest; - global $wgVariantArticlePath; - - if ( is_array( $query ) ) { - $query = wfArrayToCGI( $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 && $this->getPageLanguage()->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 ); - wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); - } - } else { - global $wgActionPaths; - $url = false; - $matches = array(); - if ( !empty( $wgActionPaths ) && - preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) - { - $action = urldecode( $matches[2] ); - if ( isset( $wgActionPaths[$action] ) ) { - $query = $matches[1]; - if ( isset( $matches[4] ) ) { - $query .= $matches[4]; - } - $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); - if ( $query != '' ) { - $url = wfAppendQuery( $url, $query ); - } - } - } - - if ( $url === false ) { - if ( $query == '-' ) { - $query = ''; - } - $url = "{$wgScript}?title={$dbkey}&{$query}"; - } - } - - wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query, $variant ) ); - - // @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' ) { - $url = $wgServer . $url; - } - } - wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query, $variant ) ); - return $url; + public function isSubpage() { + return MWNamespace::hasSubpages( $this->mNamespace ) + ? strpos( $this->getText(), '/' ) !== false + : false; } /** - * Get a URL that's the simplest URL that will be valid to link, locally, - * to the current Title. It includes the fragment, but does not include - * the server unless action=render is used (or the link is external). If - * there's a fragment but the prefixed text is empty, we just return a link - * to the fragment. - * - * The result obviously should not be URL-escaped, but does need to be - * HTML-escaped if it's being output in HTML. + * Is this a conversion table for the LanguageConverter? * - * @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). - * @return String the URL + * @return Bool */ - public function getLinkUrl( $query = array(), $variant = false ) { - wfProfileIn( __METHOD__ ); - if ( $this->isExternal() ) { - $ret = $this->getFullURL( $query ); - } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { - $ret = $this->getFragmentForURL(); - } else { - $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(); - } - wfProfileOut( __METHOD__ ); - return $ret; + public function isConversionTable() { + return $this->getNamespace() == NS_MEDIAWIKI && + strpos( $this->getText(), 'Conversiontable' ) !== false; } /** - * Get an HTML-escaped version of the URL form, suitable for - * using in a link, without a server name or fragment + * Does that page contain wikitext, or it is JS, CSS or whatever? * - * @param $query String an optional query string - * @return String the URL + * @return Bool */ - public function escapeLocalURL( $query = '' ) { - return htmlspecialchars( $this->getLocalURL( $query ) ); + public function isWikitextPage() { + $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage(); + wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) ); + return $retval; } /** - * Get an HTML-escaped version of the URL form, suitable for - * using in a link, including the server name and fragment + * Could this page contain custom CSS or JavaScript, based + * on the title? * - * @param $query String an optional query string - * @return String the URL + * @return Bool */ - public function escapeFullURL( $query = '' ) { - return htmlspecialchars( $this->getFullURL( $query ) ); + public function isCssOrJsPage() { + $retval = $this->mNamespace == NS_MEDIAWIKI + && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0; + wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) ); + return $retval; } /** - * HTML-escaped version of getCanonicalURL() + * Is this a .css or .js subpage of a user page? + * @return Bool */ - public function escapeCanonicalURL( $query = '', $variant = false ) { - return htmlspecialchars( $this->getCanonicalURL( $query, $variant ) ); + public function isCssJsSubpage() { + return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); } /** - * 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:// + * Trim down a .css or .js subpage title to get the corresponding skin name * - * @param $query String an optional query string - * @param $variant String language variant of url (for sr, zh..) - * @return String the URL + * @return string containing skin name from .css or .js subpage title */ - public function getInternalURL( $query = '', $variant = false ) { - global $wgInternalServer, $wgServer; - $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; - $url = wfExpandUrl( $server . $this->getLocalURL( $query, $variant ), PROTO_HTTP ); - wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query, $variant ) ); - return $url; + 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 ); } /** - * 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 + * Is this a .css subpage of a user page? * - * @param $query string An optional query string - * @param $variant string Language variant of URL (for sr, zh, ...) - * @return string The URL + * @return Bool */ - public function getCanonicalURL( $query = '', $variant = false ) { - $url = wfExpandUrl( $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(), PROTO_CANONICAL ); - wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query, $variant ) ); - return $url; + public function isCssSubpage() { + return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) ); } /** - * Get the edit URL for this Title + * Is this a .js subpage of a user page? * - * @return String the URL, or a null string if this is an - * interwiki link + * @return Bool */ - public function getEditURL() { - if ( $this->mInterwiki != '' ) { - return ''; - } - $s = $this->getLocalURL( 'action=edit' ); - - return $s; + public function isJsSubpage() { + return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) ); } /** - * Get the HTML-escaped displayable text form. - * Used for the title field in tags. + * Is this a talk page of some sort? * - * @return String the text, including any prefixes + * @return Bool */ - public function getEscapedText() { - return htmlspecialchars( $this->getPrefixedText() ); + public function isTalkPage() { + return MWNamespace::isTalk( $this->getNamespace() ); } /** - * Is this Title interwiki? + * Get a Title object associated with the talk page of this article * - * @return Bool + * @return Title the object for the talk page */ - public function isExternal() { - return ( $this->mInterwiki != '' ); + public function getTalkPage() { + return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); } /** - * Is this page "semi-protected" - the *only* protection is autoconfirm? + * Get a title object associated with the subject page of this + * talk page * - * @param $action String Action to check (default: edit) - * @return Bool + * @return Title the object for the subject page */ - 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; + 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() ); } /** - * Does the title correspond to a protected article? + * Get the default namespace index, for when there is no namespace * - * @param $action String the action the page is protected from, - * by default checks all actions. - * @return Bool + * @return Int Default namespace index */ - 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; + public function getDefaultNamespace() { + return $this->mDefaultNamespace; } /** - * Is this a conversion table for the LanguageConverter? + * Get title for search index * - * @return Bool + * @return String a stripped-down title string ready for the + * search index */ - public function isConversionTable() { - if( - $this->getNamespace() == NS_MEDIAWIKI && - strpos( $this->getText(), 'Conversiontable' ) !== false - ) - { - return true; + 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; + } + + /** + * Get the fragment in URL form, including the "#" character if there is one + * @return String Fragment in URL form + */ + public function getFragmentForURL() { + if ( $this->mFragment == '' ) { + return ''; + } else { + return '#' . Title::escapeFragmentForURL( $this->mFragment ); + } + } + + /** + * 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 ) ); + } + + /** + * 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; + } + + /** + * Get the prefixed database key form + * + * @return String the prefixed title, with underscores and + * any interwiki and namespace prefixes + */ + public function getPrefixedDBkey() { + $s = $this->prefix( $this->mDbkeyform ); + $s = str_replace( ' ', '_', $s ); + return $s; + } + + /** + * Get the prefixed title with spaces. + * This is the form usually used for display + * + * @return String the prefixed title, with spaces + */ + public function getPrefixedText() { + // @todo FIXME: Bad usage of empty() ? + if ( empty( $this->mPrefixedText ) ) { + $s = $this->prefix( $this->mTextform ); + $s = str_replace( '_', ' ', $s ); + $this->mPrefixedText = $s; + } + return $this->mPrefixedText; + } + + /** + * 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 '#') + * + * @return String the prefixed title, with spaces and the fragment, including '#' + */ + public function getFullText() { + $text = $this->getPrefixedText(); + if ( $this->mFragment != '' ) { + $text .= '#' . $this->mFragment; + } + return $text; + } + + /** + * Get the base page name, i.e. the leftmost part before any slashes + * + * @return String Base name + */ + public function getBaseText() { + if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { + return $this->getText(); + } + + $parts = explode( '/', $this->getText() ); + # Don't discard the real title if there's no subpage involved + if ( count( $parts ) > 1 ) { + unset( $parts[count( $parts ) - 1] ); + } + return implode( '/', $parts ); + } + + /** + * Get the lowest-level subpage name, i.e. the rightmost part after any slashes + * + * @return String Subpage name + */ + public function getSubpageText() { + if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { + return( $this->mTextform ); + } + $parts = explode( '/', $this->mTextform ); + return( $parts[count( $parts ) - 1] ); + } + + /** + * Get the HTML-escaped displayable text form. + * Used for the title field in 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 + */ + public function getSubpageUrlForm() { + $text = $this->getSubpageText(); + $text = wfUrlencode( str_replace( ' ', '_', $text ) ); + return( $text ); + } + + /** + * Get a URL-encoded title (not an actual URL) including interwiki + * + * @return String the URL-encoded form + */ + public function getPrefixedURL() { + $s = $this->prefix( $this->mDbkeyform ); + $s = wfUrlencode( str_replace( ' ', '_', $s ) ); + return $s; + } + + /** + * 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(); + * + * @since 1.19 (r105919) + * @return String + */ + 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 ); + } + 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 ); + } + // If we have $query content add a & to it first + if ( $query ) { + $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 ) ); + return $url; + } + + /** + * Get a URL with no fragment or server name. If this page is generated + * with action=render, $wgServer is prepended. + * + + * @param $query 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 = '', $query2 = false ) { + global $wgArticlePath, $wgScript, $wgServer, $wgRequest; + + $query = self::fixUrlQueryArgs( $query, $query2 ); + + $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 == '' ) { + $url = str_replace( '$1', $dbkey, $wgArticlePath ); + wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); + } else { + global $wgVariantArticlePath, $wgActionPaths; + $url = false; + $matches = array(); + + if ( !empty( $wgActionPaths ) && + preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) + { + $action = urldecode( $matches[2] ); + if ( isset( $wgActionPaths[$action] ) ) { + $query = $matches[1]; + if ( isset( $matches[4] ) ) { + $query .= $matches[4]; + } + $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); + if ( $query != '' ) { + $url = wfAppendQuery( $url, $query ); + } + } + } + + 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 = ''; + } + $url = "{$wgScript}?title={$dbkey}&{$query}"; + } + } + + 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' ) { + $url = $wgServer . $url; + } + } + wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) ); + return $url; + } + + /** + * Get a URL that's the simplest URL that will be valid to link, locally, + * to the current Title. It includes the fragment, but does not include + * the server unless action=render is used (or the link is external). If + * there's a fragment but the prefixed text is empty, we just return a link + * to the fragment. + * + * The result obviously should not be URL-escaped, but does need to be + * HTML-escaped if it's being output in HTML. + * + * See getLocalURL for the arguments. + * + * @see self::getLocalURL + * @return String the URL + */ + public function getLinkURL( $query = '', $query2 = false ) { + wfProfileIn( __METHOD__ ); + if ( $this->isExternal() ) { + $ret = $this->getFullURL( $query, $query2 ); + } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { + $ret = $this->getFragmentForURL(); + } else { + $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL(); + } + wfProfileOut( __METHOD__ ); + return $ret; + } + + /** + * Get an HTML-escaped version of the URL form, suitable for + * using in a link, without a server name or fragment + * + * See getLocalURL for the arguments. + * + * @see self::getLocalURL + * @return String the URL + */ + 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 + * + * See getLocalURL for the arguments. + * + * @see self::getLocalURL + * @return String the URL + */ + 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:// + * + * See getLocalURL for the arguments. + * + * @see self::getLocalURL + * @return String the URL + */ + 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 ), PROTO_HTTP ); + wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); + return $url; + } + + /** + * 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 + * + * See getLocalURL for the arguments. + * + * @see self::getLocalURL + * @return string The URL + * @since 1.18 + */ + 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 + * @return string + */ + 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 + * interwiki link + */ + public function getEditURL() { + if ( $this->mInterwiki != '' ) { + return ''; } + $s = $this->getLocalURL( 'action=edit' ); - return false; + return $s; } /** @@ -1169,7 +1520,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 * @@ -1179,42 +1542,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 ); - } - - /** - * 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 - */ - public function isNamespaceProtected( User $user ) { - global $wgNamespaceProtection; - - if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { - foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { - if ( $right != '' && !$user->isAllowed( $right ) ) { - return true; - } - } - } - return false; + public function quickUserCan( $action, $user = null ) { + return $this->userCan( $action, $user, false ); } /** - * Can $wgUser perform $action on this page? + * Can $user 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. + * @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 userCan( $action, $doExpensiveQueries = true ) { - global $wgUser; - return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array() ); + public function userCan( $action, $user = null, $doExpensiveQueries = true ) { + if ( !$user instanceof User ) { + global $wgUser; + $user = $wgUser; + } + return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) ); } /** @@ -1224,9 +1575,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() ) { @@ -1256,33 +1608,34 @@ class Title { * @return Array list of errors */ private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { - $ns = $this->getNamespace(); - if ( $action == 'create' ) { - if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk', $ns ) ) || - ( !$this->isTalkPage() && !$user->isAllowed( 'createpage', $ns ) ) ) { + if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || + ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) { $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); } } elseif ( $action == 'move' ) { - if ( !$user->isAllowed( 'move-rootuserpages', $ns ) - && $ns == NS_USER && !$this->isSubpage() ) { + if ( !$user->isAllowed( 'move-rootuserpages' ) + && $this->mNamespace == NS_USER && !$this->isSubpage() ) { // Show user page-specific message only if the user can move other pages $errors[] = array( 'cant-move-user-page' ); } // Check if user is allowed to move files if it's a file - if ( $ns == NS_FILE && !$user->isAllowed( 'movefile', $ns ) ) { + if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { $errors[] = array( 'movenotallowedfile' ); } - if ( !$user->isAllowed( 'move', $ns) ) { + if ( !$user->isAllowed( 'move' ) ) { // User can't move anything - - $userCanMove = in_array( 'move', User::getGroupPermissions( - array( 'user' ), $ns ), true ); - $autoconfirmedCanMove = in_array( 'move', User::getGroupPermissions( - array( 'autoconfirmed' ), $ns ), true ); - + global $wgGroupPermissions; + $userCanMove = false; + if ( isset( $wgGroupPermissions['user']['move'] ) ) { + $userCanMove = $wgGroupPermissions['user']['move']; + } + $autoconfirmedCanMove = false; + if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) { + $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move']; + } if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { // custom message if logged-in users without any special rights can move $errors[] = array( 'movenologintext' ); @@ -1291,33 +1644,16 @@ class Title { } } } elseif ( $action == 'move-target' ) { - if ( !$user->isAllowed( 'move', $ns ) ) { + if ( !$user->isAllowed( 'move' ) ) { // User can't move anything $errors[] = array( 'movenotallowed' ); - } elseif ( !$user->isAllowed( 'move-rootuserpages', $ns ) - && $ns == NS_USER && !$this->isSubpage() ) { + } elseif ( !$user->isAllowed( 'move-rootuserpages' ) + && $this->mNamespace == NS_USER && !$this->isSubpage() ) { // Show user page-specific message only if the user can move other pages $errors[] = array( 'cant-move-to-user-page' ); } - } elseif ( !$user->isAllowed( $action, $ns ) ) { - // We avoid expensive display logic for quickUserCan's and such - $groups = false; - if ( !$short ) { - $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), - User::getGroupsWithPermission( $action, $ns ) ); - } - - if ( $groups ) { - global $wgLang; - $return = array( - 'badaccess-groups', - $wgLang->commaList( $groups ), - count( $groups ) - ); - } else { - $return = array( 'badaccess-group0' ); - } - $errors[] = $return; + } elseif ( !$user->isAllowed( $action ) ) { + $errors[] = $this->missingPermissionError( $action, $short ); } return $errors; @@ -1392,7 +1728,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' ); } @@ -1454,15 +1790,12 @@ class Title { if ( $right == 'sysop' ) { $right = 'protect'; } - if ( $right != '' && !$user->isAllowed( $right, $this->mNamespace ) ) { + if ( $right != '' && !$user->isAllowed( $right ) ) { // Users with 'editprotected' permission can edit protected pages - if ( $action == 'edit' && $user->isAllowed( 'editprotected', $this->mNamespace ) ) { - // 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 ); } } @@ -1497,7 +1830,7 @@ class Title { if ( isset( $restrictions[$action] ) ) { foreach ( $restrictions[$action] as $right ) { $right = ( $right == 'sysop' ) ? 'protect' : $right; - if ( $right != '' && !$user->isAllowed( $right, $this->mNamespace ) ) { + if ( $right != '' && !$user->isAllowed( $right ) ) { $pages = ''; foreach ( $cascadingSources as $page ) $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; @@ -1522,8 +1855,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' ); } @@ -1534,8 +1869,8 @@ class Title { $title_protection['pt_create_perm'] = 'protect'; // B/C } if( $title_protection['pt_create_perm'] == '' || - !$user->isAllowed( $title_protection['pt_create_perm'], - $this->mNamespace ) ) { + !$user->isAllowed( $title_protection['pt_create_perm'] ) ) + { $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] ); } } @@ -1554,6 +1889,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; } @@ -1570,21 +1911,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 ) { @@ -1626,181 +1965,22 @@ class Title { } /** - * 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) + * Check that the user is allowed to read this page. * - * @param $action String action that permission needs to be checked for + * @param $action String the action to check * @param $user User to check - * @param $doExpensiveQueries Bool Set this to false to avoid doing unnecessary queries. - * @param $short Bool Set this to true to stop after the first permission error. - * @return Array of arrays of the arguments to wfMsg to explain permissions problems. - */ - protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { - wfProfileIn( __METHOD__ ); - - $errors = array(); - $checks = array( - 'checkQuickPermissions', - 'checkPermissionHooks', - 'checkSpecialsAndNSPermissions', - 'checkCSSandJSPermissions', - 'checkPageRestrictions', - 'checkCascadingSourcesRestrictions', - 'checkActionPermissions', - 'checkUserBlock' - ); - - while( count( $checks ) > 0 && - !( $short && count( $errors ) > 0 ) ) { - $method = array_shift( $checks ); - $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); - } - - wfProfileOut( __METHOD__ ); - return $errors; - } - - /** - * Is this title subject to title protection? - * Title protection is the one applied against creation of such title. - * - * @return Mixed An associative array representing any existent title - * protection, or false if there's none. - */ - private function getTitleProtection() { - // Can't protect pages in special namespaces - if ( $this->getNamespace() < 0 ) { - return false; - } - - // Can't protect pages that exist. - if ( $this->exists() ) { - return false; - } - - if ( !isset( $this->mTitleProtection ) ) { - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'protected_titles', '*', - array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), - __METHOD__ ); - - // fetchRow returns false if there are no rows. - $this->mTitleProtection = $dbr->fetchRow( $res ); - } - return $this->mTitleProtection; - } - - /** - * Update the title protection status - * - * @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 ); - - $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; - } - - # Update the protection log - if ( $dbw->affectedRows() ) { - $log = new LogPage( 'protect' ); - - 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 ); - } - } - - return true; - } - - /** - * Remove any title protection due to page existing - */ - public function deleteTitleProtection() { - $dbw = wfGetDB( DB_MASTER ); - - $dbw->delete( - 'protected_titles', - array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), - __METHOD__ - ); - $this->mTitleProtection = false; - } - - /** - * 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; - } - - /** - * Can $wgUser read this page? + * @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 Bool - * @todo fold these checks into userCan() + * @return Array list of errors */ - public function userCanRead() { - global $wgUser, $wgGroupPermissions; - + 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 ) ) { - global $wgRevokePermissions; $useShortcut = true; if ( empty( $wgGroupPermissions['*']['read'] ) ) { # Not a public wiki, so no shortcut @@ -1822,253 +2002,348 @@ class Title { } } - $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 + $whitelisted = false; if ( $useShortcut ) { - return true; - } - - if ( $wgUser->isAllowed( 'read' ) ) { - return true; - } else { - global $wgWhitelistRead; - + # 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. - if ( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'ChangePassword' ) ) { - return true; - } - - # Bail out if there isn't whitelist - if ( !is_array( $wgWhitelistRead ) ) { - return false; - } - - # Check for explicit whitelisting + $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 ) ) - return true; - # Old settings might have the title prefixed with - # a colon for main-namespace pages - if ( $this->getNamespace() == NS_MAIN ) { + // Check for explicit whitelisting with and without underscores + if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { + $whitelisted = true; + } elseif ( $this->getNamespace() == NS_MAIN ) { + # Old settings might have the title prefixed with + # a colon for main-namespace pages if ( in_array( ':' . $name, $wgWhitelistRead ) ) { - return true; + $whitelisted = true; } - } - - # If it's a special page, ditch the subpage bit and check again - if ( $this->getNamespace() == NS_SPECIAL ) { + } 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 ) { - # 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 ( $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 false; + + return $errors; } /** - * 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(); + * Get a description array when the user doesn't have the right to perform + * $action (i.e. when User::isAllowed() returns false) * - * @since 1.18 - * @return Bool + * @param $action String the action to check + * @param $short Boolean short circuit on first error + * @return Array list of errors */ - public function isMainPage() { - return $this->equals( Title::newMainPage() ); + 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' ); + } } /** - * Is this a talk page of some sort? + * 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) * - * @return Bool + * @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. + * @param $short Bool Set this to true to stop after the first permission error. + * @return Array of arrays of the arguments to wfMsg to explain permissions problems. */ - public function isTalkPage() { - return MWNamespace::isTalk( $this->getNamespace() ); + protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { + wfProfileIn( __METHOD__ ); + + # 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 ) ) { + $method = array_shift( $checks ); + $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); + } + + wfProfileOut( __METHOD__ ); + return $errors; } /** - * Is this a subpage? + * 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 isSubpage() { - return MWNamespace::hasSubpages( $this->mNamespace ) - ? strpos( $this->getText(), '/' ) !== false - : false; + public function userCanEditCssSubpage() { + global $wgUser; + wfDeprecated( __METHOD__, '1.19' ); + return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) ) + || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); } /** - * Does this have subpages? (Warning, usually requires an extra DB query.) + * 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 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 userCanEditJsSubpage() { + global $wgUser; + wfDeprecated( __METHOD__, '1.19' ); + return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) ) + || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); + } - $subpages = $this->getSubpages( 1 ); - if ( $subpages instanceof TitleArray ) { - return $this->mHasSubpages = (bool)$subpages->count(); + /** + * 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 $this->mHasSubpages = false; + return $types; } /** - * Get all subpages of this page. + * Returns restriction types for the current Title * - * @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 + * @return array applicable restriction types */ - public function getSubpages( $limit = -1 ) { - if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { + public function getRestrictionTypes() { + if ( $this->isSpecialPage() ) { return array(); } - $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; + $types = self::getFilteredRestrictionTypes( $this->exists() ); + + if ( $this->getNamespace() != NS_FILE ) { + # Remove the upload restriction for non-file titles + $types = array_diff( $types, array( 'upload' ) ); } - return $this->mSubpages = TitleArray::newFromResult( - $dbr->select( 'page', - array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), - $conds, - __METHOD__, - $options - ) - ); + + wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); + + wfDebug( __METHOD__ . ': applicable restrictions to [[' . + $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); + + return $types; } /** - * Does that page contain wikitext, or it is JS, CSS or whatever? + * Is this title subject to title protection? + * Title protection is the one applied against creation of such title. * - * @return Bool + * @return Mixed An associative array representing any existent title + * protection, or false if there's none. */ - public function isWikitextPage() { - $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage(); - wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) ); - return $retval; + private function getTitleProtection() { + // Can't protect pages in special namespaces + if ( $this->getNamespace() < 0 ) { + return false; + } + + // Can't protect pages that exist. + if ( $this->exists() ) { + return false; + } + + if ( !isset( $this->mTitleProtection ) ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'protected_titles', '*', + array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), + __METHOD__ ); + + // fetchRow returns false if there are no rows. + $this->mTitleProtection = $dbr->fetchRow( $res ); + } + return $this->mTitleProtection; } /** - * Could this page contain custom CSS or JavaScript, based - * on the title? + * Update the title protection status * - * @return Bool + * @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 isCssOrJsPage() { - $retval = $this->mNamespace == NS_MEDIAWIKI - && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0; - wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) ); - return $retval; - } + public function updateTitleProtection( $create_perm, $reason, $expiry ) { + wfDeprecated( __METHOD__, '1.19' ); - /** - * 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 ) ); + global $wgUser; + + $limit = array( 'create' => $create_perm ); + $expiry = array( 'create' => $expiry ); + + $page = WikiPage::factory( $this ); + $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser ); + + return $status->isOK(); } /** - * Is this a *valid* .css or .js subpage of a user page? - * - * @return Bool - * @deprecated since 1.17 + * Remove any title protection due to page existing */ - public function isValidCssJsSubpage() { - return $this->isCssJsSubpage(); + public function deleteTitleProtection() { + $dbw = wfGetDB( DB_MASTER ); + + $dbw->delete( + 'protected_titles', + array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), + __METHOD__ + ); + $this->mTitleProtection = false; } /** - * Trim down a .css or .js subpage title to get the corresponding skin name + * Is this page "semi-protected" - the *only* protection is autoconfirm? * - * @return string containing skin name from .css or .js subpage title + * @param $action String Action to check (default: edit) + * @return Bool */ - public function getSkinFromCssJsSubpage() { - $subpage = explode( '/', $this->mTextform ); - $subpage = $subpage[ count( $subpage ) - 1 ]; - return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) ); + 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; + } } /** - * Is this a .css subpage of a user page? + * 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 isCssSubpage() { - return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) ); - } + public function isProtected( $action = '' ) { + global $wgRestrictionLevels; + + $restrictionTypes = $this->getRestrictionTypes(); + + # Special pages have inherent protection + if( $this->isSpecialPage() ) { + return true; + } + + # Check regular protection levels + foreach ( $restrictionTypes as $type ) { + if ( $action == $type || $action == '' ) { + $r = $this->getRestrictions( $type ); + foreach ( $wgRestrictionLevels as $level ) { + if ( in_array( $level, $r ) && $level != '' ) { + return true; + } + } + } + } - /** - * 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. * - * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead. + * @param $user User object to check permissions * @return Bool */ - public function userCanEditCssSubpage() { - global $wgUser; - wfDeprecated( __METHOD__ ); - 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? - * - * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead. - * @return Bool - */ - public function userCanEditJsSubpage() { - global $wgUser; - wfDeprecated( __METHOD__ ); - 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; } /** @@ -2179,6 +2454,34 @@ class Title { return array( $sources, $pagerestrictions ); } + /** + * 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 * @@ -2334,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 */ @@ -2353,31 +2665,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 + ) + ); } /** @@ -2520,9 +2859,9 @@ 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 */ @@ -2540,73 +2879,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; } /** @@ -2824,45 +3097,65 @@ 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 "#". + * Get an array of Title objects linking to this Title + * Also stores the IDs in the link cache. * - * Deprecated for public use, use Title::makeTitle() with fragment parameter. - * Still in active use privately. + * WARNING: do not use this function on arbitrary user-supplied titles! + * On heavily-used templates it will max out the memory. * - * @param $fragment String text + * @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 setFragment( $fragment ) { - $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); - } + public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { + if ( count( $options ) > 0 ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_SLAVE ); + } - /** - * 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() ); + $res = $db->select( + array( 'page', $table ), + array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), + array( + "{$prefix}_from=page_id", + "{$prefix}_namespace" => $this->getNamespace(), + "{$prefix}_title" => $this->getDBkey() ), + __METHOD__, + $options + ); + + $retVal = array(); + if ( $res->numRows() ) { + $linkCache = LinkCache::singleton(); + foreach ( $res as $row ) { + $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ); + if ( $titleObj ) { + $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); + $retVal[] = $titleObj; + } + } + } + return $retVal; } /** - * Get a title object associated with the subject page of this - * talk page + * Get an array of Title objects using this Title as a template + * Also stores the IDs in the link cache. * - * @return Title the object for the subject page + * 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 linking here */ - 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() ); + public function getTemplateLinksTo( $options = array() ) { + return $this->getLinksTo( $options, 'templatelinks', 'tl' ); } /** - * Get an array of Title objects linking to this 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! @@ -2873,8 +3166,13 @@ class Title { * @param $prefix String: fields prefix * @return Array of Title objects linking here */ - public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { - $linkCache = LinkCache::singleton(); + 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 ); @@ -2882,23 +3180,29 @@ class Title { $db = wfGetDB( DB_SLAVE ); } + $namespaceFiled = "{$prefix}_namespace"; + $titleField = "{$prefix}_title"; + $res = $db->select( - array( 'page', $table ), - array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), - array( - "{$prefix}_from=page_id", - "{$prefix}_namespace" => $this->getNamespace(), - "{$prefix}_title" => $this->getDBkey() ), + array( $table, 'page' ), + array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), + array( "{$prefix}_from" => $id ), __METHOD__, - $options + $options, + array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) ) ); $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 ); + $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); if ( $titleObj ) { - $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); + if ( $row->page_id ) { + $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); + } else { + $linkCache->addBadLinkObj( $titleObj ); + } $retVal[] = $titleObj; } } @@ -2907,17 +3211,17 @@ class Title { } /** - * Get an array of Title objects using this Title as a template + * 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 linking here + * @return Array of Title the Title objects used here */ - public function getTemplateLinksTo( $options = array() ) { - return $this->getLinksTo( $options, 'templatelinks', 'tl' ); + public function getTemplateLinksFrom( $options = array() ) { + return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); } /** @@ -3148,8 +3452,11 @@ class Title { * @return Mixed true on success, getUserPermissionsErrors()-like array on failure */ public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { + global $wgUser; $err = $this->isValidMoveOperation( $nt, $auth, $reason ); if ( is_array( $err ) ) { + // Auto-block user's IP if the account was "hard" blocked + $wgUser->spreadAnyEditBlock(); return $err; } @@ -3164,15 +3471,14 @@ class Title { return $status->getErrorsArray(); } } + // Clear RepoGroup process cache + RepoGroup::singleton()->clearCache( $this ); + RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache } - // 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 ); @@ -3182,8 +3488,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( @@ -3207,6 +3511,8 @@ class Title { ); } + $redirid = $this->getArticleID(); + if ( $protected ) { # Protect the redirect title as the title used to be... $dbw->insertSelect( 'page_restrictions', 'page_restrictions', @@ -3242,51 +3548,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; } @@ -3336,38 +3599,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( 'redirect', array( 'rd_from' => $newid ), __METHOD__ ); - $dbw->delete( 'page_props', array( 'pp_page' => $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 @@ -3377,30 +3620,33 @@ 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 ); + $newpage->updateRevisionOn( $dbw, $nullRevision ); + wfRunHooks( 'NewRevisionFromEditComplete', - array( $article, $nullRevision, $latest, $wgUser ) ); - $article->setCachedLastEditTime( $now ); + array( $newpage, $nullRevision, $latest, $wgUser ) ); + + $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) ); # 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 ); if ( $newid ) { // sanity $redirectRevision = new Revision( array( @@ -3413,33 +3659,13 @@ class Title { 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__ ); + $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) ); } - } else { - $this->resetArticleID( 0 ); } # Log the move $logid = $logEntry->insert(); $logEntry->publish( $logid ); - - # 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(); } /** @@ -3575,6 +3801,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? @@ -3595,15 +3824,6 @@ class Title { return true; } - /** - * 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. @@ -3764,6 +3984,41 @@ class Title { return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); } + /** + * 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. @@ -3849,31 +4104,6 @@ class Title { && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; } - /** - * 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 - * - * @return String representation of this title - */ - public function __toString() { - return $this->getPrefixedText(); - } - /** * Check if page exists. For historical reasons, this function simply * checks for the existence of the title in the page table, and will @@ -3935,7 +4165,21 @@ class Title { * @return Bool */ public function isKnown() { - return $this->isAlwaysKnown() || $this->exists(); + $isKnown = null; + + /** + * Allows overriding default behaviour for determining if a page exists. + * If $isKnown is kept as null, regular checks happen. If it's + * a boolean, this value is returned by the isKnown method. + * + * @since 1.20 + * + * @param Title $title + * @param boolean|null $isKnown + */ + wfRunHooks( 'TitleIsKnown', array( $this, &$isKnown ) ); + + return is_null( $isKnown ) ? ( $this->isAlwaysKnown() || $this->exists() ) : $isKnown; } /** @@ -3985,13 +4229,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; } /** @@ -4058,46 +4312,6 @@ class Title { return $this->mNotificationTimestamp[$uid]; } - /** - * 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 ""; - } - /** * Generate strings used for xml 'id' names in monobook tabs * @@ -4129,61 +4343,6 @@ class Title { return $prepend . $namespaceKey; } - /** - * 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 * @@ -4241,7 +4400,7 @@ class Title { /** * Get a backlink cache object * - * @return object BacklinkCache + * @return BacklinkCache */ function getBacklinkCache() { if ( is_null( $this->mBacklinkCache ) ) { @@ -4266,50 +4425,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 @@ -4349,7 +4464,7 @@ 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->isCssOrJsPage() ) { @@ -4367,4 +4482,4 @@ class Title { wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) ); return wfGetLangObj( $pageLang ); } -} \ No newline at end of file +}