X-Git-Url: https://git.cyclocoop.org/%27.WWW_URL.%27admin/?a=blobdiff_plain;f=includes%2FTitle.php;h=eae188cfcbc1b34c9209c813a2132b9085cb570d;hb=a15c419b3d130248f2556b9d00643ba9666a4189;hp=535b07674af1bc419a433d7ecc2acba47ef7c8b1;hpb=08d29e055d4be3e38c87a1f9bd5b2a7ea9f8d50b;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Title.php b/includes/Title.php index 535b07674a..eae188cfcb 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -2,11 +2,12 @@ /** * See title.txt * - * @package MediaWiki */ /** */ -require_once( 'normal/UtfNormal.php' ); +if ( !class_exists( 'UtfNormal' ) ) { + require_once( dirname(__FILE__) . '/normal/UtfNormal.php' ); +} define ( 'GAID_FOR_UPDATE', 1 ); @@ -17,12 +18,14 @@ define ( 'GAID_FOR_UPDATE', 1 ); # reset the cache. define( 'MW_TITLECACHE_MAX', 1000 ); +# Constants for pr_cascade bitfield +define( 'CASCADE', 1 ); + /** * Title class * - Represents a title, which may contain an interwiki designation or namespace * - Can fetch various kinds of data from the database, albeit inefficiently. * - * @package MediaWiki */ class Title { /** @@ -37,25 +40,29 @@ class Title { * Please use the accessor functions */ - /**#@+ + /**#@+ * @private */ - var $mTextform; # Text form (spaces not underscores) of the main part - var $mUrlform; # URL-encoded form of the main part - var $mDbkeyform; # Main part with underscores - var $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants - var $mInterwiki; # Interwiki prefix (or null string) - var $mFragment; # Title fragment (i.e. the bit after the #) - var $mArticleID; # Article ID, fetched from the link cache on demand - var $mLatestID; # ID of most recent revision - var $mRestrictions; # Array of groups allowed to edit this article - # Only null or "sysop" are supported - var $mRestrictionsLoaded; # Boolean for initialisation on demand - var $mPrefixedText; # Text form including namespace/interwiki, initialised on demand - var $mDefaultNamespace; # Namespace index when there is no namespace - # Zero except in {{transclusion}} tags - var $mWatched; # Is $wgUser watching this page? NULL if unfilled, accessed through userIsWatching() + var $mTextform; # Text form (spaces not underscores) of the main part + var $mUrlform; # URL-encoded form of the main part + var $mDbkeyform; # Main part with underscores + var $mUserCaseDBKey; # DB key with the initial letter in the case specified by the user + var $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants + var $mInterwiki; # Interwiki prefix (or null string) + var $mFragment; # Title fragment (i.e. the bit after the #) + var $mArticleID; # Article ID, fetched from the link cache on demand + var $mLatestID; # ID of most recent revision + var $mRestrictions; # Array of groups allowed to edit this article + var $mCascadeRestriction; # Cascade restrictions on this page to included templates and images? + var $mRestrictionsExpiry; # When do the restrictions on this page expire? + var $mHasCascadingRestrictions; # Are cascading restrictions in effect on this page? + var $mCascadeRestrictionSources;# Where are the cascading restrictions coming from on this page? + var $mRestrictionsLoaded; # Boolean for initialisation on demand + var $mPrefixedText; # Text form including namespace/interwiki, initialised on demand + var $mDefaultNamespace; # Namespace index when there is no namespace + # Zero except in {{transclusion}} tags + var $mWatched; # Is $wgUser watching this page? NULL if unfilled, accessed through userIsWatching() /**#@-*/ @@ -63,7 +70,7 @@ class Title { * Constructor * @private */ - /* private */ function Title() { + /* private */ function __construct() { $this->mInterwiki = $this->mUrlform = $this->mTextform = $this->mDbkeyform = ''; $this->mArticleID = -1; @@ -75,6 +82,7 @@ class Title { $this->mDefaultNamespace = NS_MAIN; $this->mWatched = NULL; $this->mLatestID = false; + $this->mOldRestrictions = false; } /** @@ -83,10 +91,8 @@ class Title { * instead of spaces, possibly including namespace and * interwiki prefixes * @return Title the new object, or NULL on an error - * @static - * @access public */ - /* static */ function newFromDBkey( $key ) { + public static function newFromDBkey( $key ) { $t = new Title(); $t->mDbkeyform = $key; if( $t->secureAndSplit() ) @@ -105,12 +111,8 @@ class Title { * @param int $defaultNamespace the namespace to use if * none is specified by a prefix * @return Title the new object, or NULL on an error - * @static - * @access public */ - function newFromText( $text, $defaultNamespace = NS_MAIN ) { - $fname = 'Title::newFromText'; - + public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { if( is_object( $text ) ) { throw new MWException( 'Title::newFromText given an object' ); } @@ -132,7 +134,7 @@ class Title { */ $filteredText = Sanitizer::decodeCharReferences( $text ); - $t =& new Title(); + $t = new Title(); $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); $t->mDefaultNamespace = $defaultNamespace; @@ -159,10 +161,8 @@ class Title { * the given title's length does not exceed the maximum. * @param string $url the title, as might be taken from a URL * @return Title the new object, or NULL on an error - * @static - * @access public */ - function newFromURL( $url ) { + public static function newFromURL( $url ) { global $wgLegalTitleChars; $t = new Title(); @@ -189,12 +189,10 @@ class Title { * * @param int $id the page_id corresponding to the Title to create * @return Title the new object, or NULL on an error - * @access public - * @static */ - function newFromID( $id ) { + public static function newFromID( $id ) { $fname = 'Title::newFromID'; - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'page', array( 'page_namespace', 'page_title' ), array( 'page_id' => $id ), $fname ); if ( $row !== false ) { @@ -208,8 +206,8 @@ class Title { /** * Make an array of titles from an array of IDs */ - function newFromIDs( $ids ) { - $dbr =& wfGetDB( DB_SLAVE ); + public static function newFromIDs( $ids ) { + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'page', array( 'page_namespace', 'page_title' ), 'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ ); @@ -230,14 +228,12 @@ class Title { * @param int $ns the namespace of the article * @param string $title the unprefixed database key form * @return Title the new object - * @static - * @access public */ - function &makeTitle( $ns, $title ) { - $t =& new Title(); + public static function &makeTitle( $ns, $title ) { + $t = new Title(); $t->mInterwiki = ''; $t->mFragment = ''; - $t->mNamespace = intval( $ns ); + $t->mNamespace = $ns = intval( $ns ); $t->mDbkeyform = str_replace( ' ', '_', $title ); $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; $t->mUrlform = wfUrlencode( $t->mDbkeyform ); @@ -246,17 +242,15 @@ class Title { } /** - * Create a new Title frrom a namespace index and a DB key. + * Create a new Title from a namespace index and a DB key. * The parameters will be checked for validity, which is a bit slower * than makeTitle() but safer for user-provided data. * * @param int $ns the namespace of the article * @param string $title the database key form * @return Title the new object, or NULL on an error - * @static - * @access public */ - function makeTitleSafe( $ns, $title ) { + public static function makeTitleSafe( $ns, $title ) { $t = new Title(); $t->mDbkeyform = Title::makeName( $ns, $title ); if( $t->secureAndSplit() ) { @@ -268,12 +262,9 @@ class Title { /** * Create a new Title for the Main Page - * - * @static * @return Title the new object - * @access public */ - function newMainPage() { + public static function newMainPage() { return Title::newFromText( wfMsgForContent( 'mainpage' ) ); } @@ -282,13 +273,12 @@ class Title { * @param string $text the redirect title text * @return Title the new object, or NULL if the text is not a * valid redirect - * @static - * @access public */ - function newFromRedirect( $text ) { - global $wgMwRedir; + public static function newFromRedirect( $text ) { + $mwRedir = MagicWord::get( 'redirect' ); $rt = NULL; - if ( $wgMwRedir->matchStart( $text ) ) { + if ( $mwRedir->matchStart( $text ) ) { + $m = array(); if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) { # categories are escaped using : for example one can enter: # #REDIRECT [[:Category:Music]]. Need to remove it. @@ -299,7 +289,7 @@ class Title { $rt = Title::newFromText( $m[1] ); # Disallow redirects to Special:Userlogout - if ( !is_null($rt) && $rt->getNamespace() == NS_SPECIAL && preg_match( '/^Userlogout/i', $rt->getText() ) ) { + if ( !is_null($rt) && $rt->isSpecial( 'Userlogout' ) ) { $rt = NULL; } } @@ -321,7 +311,7 @@ class Title { */ function nameOf( $id ) { $fname = 'Title::nameOf'; - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $s = $dbr->selectRow( 'page', array( 'page_namespace','page_title' ), array( 'page_id' => $id ), $fname ); if ( $s === false ) { return NULL; } @@ -333,10 +323,8 @@ class Title { /** * Get a regex character class describing the legal characters in a link * @return string the list of characters, not delimited - * @static - * @access public */ - function legalChars() { + public static function legalChars() { global $wgLegalTitleChars; return $wgLegalTitleChars; } @@ -350,13 +338,13 @@ class Title { * @return string a stripped-down title string ready for the * search index */ - /* static */ function indexTitle( $ns, $title ) { + public static function indexTitle( $ns, $title ) { global $wgContLang; $lc = SearchEngine::legalSearchChars() . '&#;'; $t = $wgContLang->stripForSearch( $title ); $t = preg_replace( "/[^{$lc}]+/", ' ', $t ); - $t = strtolower( $t ); + $t = $wgContLang->lc( $t ); # Handle 's, s' $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t ); @@ -376,7 +364,7 @@ class Title { * @param string $title the DB key form the title * @return string the prefixed form of the title */ - /* static */ function makeName( $ns, $title ) { + public static function makeName( $ns, $title ) { global $wgContLang; $n = $wgContLang->getNsText( $ns ); @@ -389,16 +377,15 @@ class Title { * @return the associated URL, containing "$1", which should be * replaced by an article title * @static (arguably) - * @access public */ - function getInterwikiLink( $key ) { - global $wgMemc, $wgDBname, $wgInterwikiExpiry; - global $wgInterwikiCache; + public function getInterwikiLink( $key ) { + global $wgMemc, $wgInterwikiExpiry; + global $wgInterwikiCache, $wgContLang; $fname = 'Title::getInterwikiLink'; - $key = strtolower( $key ); + $key = $wgContLang->lc( $key ); - $k = $wgDBname.':interwiki:'.$key; + $k = wfMemcKey( 'interwiki', $key ); if( array_key_exists( $k, Title::$interwikiCache ) ) { return Title::$interwikiCache[$k]->iw_url; } @@ -414,7 +401,7 @@ class Title { return $s->iw_url; } - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'interwiki', array( 'iw_url', 'iw_local', 'iw_trans' ), array( 'iw_prefix' => $key ), $fname ); @@ -442,21 +429,20 @@ class Title { * More logic is explained in DefaultSettings * * @return string URL of interwiki site - * @access public */ - function getInterwikiCached( $key ) { - global $wgDBname, $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite; + public static function getInterwikiCached( $key ) { + global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite; static $db, $site; if (!$db) $db=dba_open($wgInterwikiCache,'r','cdb'); /* Resolve site name */ if ($wgInterwikiScopes>=3 and !$site) { - $site = dba_fetch("__sites:{$wgDBname}", $db); + $site = dba_fetch('__sites:' . wfWikiID(), $db); if ($site=="") $site = $wgInterwikiFallbackSite; } - $value = dba_fetch("{$wgDBname}:{$key}", $db); + $value = dba_fetch( wfMemcKey( $key ), $db); if ($value=='' and $wgInterwikiScopes>=3) { /* try site-level */ $value = dba_fetch("_{$site}:{$key}", $db); @@ -476,7 +462,7 @@ class Title { $s->iw_url=$url; $s->iw_local=(int)$local; } - Title::$interwikiCache[$wgDBname.':interwiki:'.$key] = $s; + Title::$interwikiCache[wfMemcKey( 'interwiki', $key )] = $s; return $s->iw_url; } /** @@ -485,15 +471,12 @@ class Title { * * @return bool TRUE if this is an in-project interwiki link * or a wikilink, FALSE otherwise - * @access public */ - function isLocal() { - global $wgDBname; - + public function isLocal() { if ( $this->mInterwiki != '' ) { # Make sure key is loaded into cache $this->getInterwikiLink( $this->mInterwiki ); - $k = $wgDBname.':interwiki:' . $this->mInterwiki; + $k = wfMemcKey( 'interwiki', $this->mInterwiki ); return (bool)(Title::$interwikiCache[$k]->iw_local); } else { return true; @@ -505,71 +488,27 @@ class Title { * this project and is transcludable. * * @return bool TRUE if this is transcludable - * @access public */ - function isTrans() { - global $wgDBname; - + public function isTrans() { if ($this->mInterwiki == '') return false; # Make sure key is loaded into cache $this->getInterwikiLink( $this->mInterwiki ); - $k = $wgDBname.':interwiki:' . $this->mInterwiki; + $k = wfMemcKey( 'interwiki', $this->mInterwiki ); return (bool)(Title::$interwikiCache[$k]->iw_trans); } /** - * Update the page_touched field for an array of title objects - * @todo Inefficient unless the IDs are already loaded into the - * link cache - * @param array $titles an array of Title objects to be touched - * @param string $timestamp the timestamp to use instead of the - * default current time - * @static - * @access public + * Escape a text fragment, say from a link, for a URL */ - function touchArray( $titles, $timestamp = '' ) { - - if ( count( $titles ) == 0 ) { - return; - } - $dbw =& wfGetDB( DB_MASTER ); - if ( $timestamp == '' ) { - $timestamp = $dbw->timestamp(); - } - /* - $page = $dbw->tableName( 'page' ); - $sql = "UPDATE $page SET page_touched='{$timestamp}' WHERE page_id IN ("; - $first = true; - - foreach ( $titles as $title ) { - if ( $wgUseFileCache ) { - $cm = new CacheManager($title); - @unlink($cm->fileCacheName()); - } - - if ( ! $first ) { - $sql .= ','; - } - $first = false; - $sql .= $title->getArticleID(); - } - $sql .= ')'; - if ( ! $first ) { - $dbw->query( $sql, 'Title::touchArray' ); - } - */ - // hack hack hack -- brion 2005-07-11. this was unfriendly to db. - // do them in small chunks: - $fname = 'Title::touchArray'; - foreach( $titles as $title ) { - $dbw->update( 'page', - array( 'page_touched' => $timestamp ), - array( - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() ), - $fname ); - } + static function escapeFragmentForURL( $fragment ) { + $fragment = str_replace( ' ', '_', $fragment ); + $fragment = urlencode( Sanitizer::decodeCharReferences( $fragment ) ); + $replaceArray = array( + '%3A' => ':', + '%' => '.' + ); + return strtr( $fragment, $replaceArray ); } #---------------------------------------------------------------------------- @@ -580,42 +519,54 @@ class Title { /** * Get the text form (spaces not underscores) of the main part * @return string - * @access public */ - function getText() { return $this->mTextform; } + public function getText() { return $this->mTextform; } /** * Get the URL-encoded form of the main part * @return string - * @access public */ - function getPartialURL() { return $this->mUrlform; } + public function getPartialURL() { return $this->mUrlform; } /** * Get the main part with underscores * @return string - * @access public */ - function getDBkey() { return $this->mDbkeyform; } + public function getDBkey() { return $this->mDbkeyform; } /** * Get the namespace index, i.e. one of the NS_xxxx constants * @return int - * @access public */ - function getNamespace() { return $this->mNamespace; } + public function getNamespace() { return $this->mNamespace; } /** * Get the namespace text * @return string - * @access public */ - function getNsText() { - global $wgContLang; + public function getNsText() { + global $wgContLang, $wgCanonicalNamespaceNames; + + if ( '' != $this->mInterwiki ) { + // This probably shouldn't even happen. ohh man, oh yuck. + // But for interwiki transclusion it sometimes does. + // Shit. Shit shit shit. + // + // Use the canonical namespaces if possible to try to + // resolve a foreign namespace. + if( isset( $wgCanonicalNamespaceNames[$this->mNamespace] ) ) { + return $wgCanonicalNamespaceNames[$this->mNamespace]; + } + } return $wgContLang->getNsText( $this->mNamespace ); } + /** + * Get the DB key with the initial letter case as specified by the user + */ + function getUserCaseDBKey() { + return $this->mUserCaseDBKey; + } /** * Get the namespace text of the subject (rather than talk) page * @return string - * @access public */ - function getSubjectNsText() { + public function getSubjectNsText() { global $wgContLang; return $wgContLang->getNsText( Namespace::getSubject( $this->mNamespace ) ); } @@ -624,44 +575,52 @@ class Title { * Get the namespace text of the talk page * @return string */ - function getTalkNsText() { + public function getTalkNsText() { global $wgContLang; return( $wgContLang->getNsText( Namespace::getTalk( $this->mNamespace ) ) ); } - + /** * Could this title have a corresponding talk page? * @return bool */ - function canTalk() { + public function canTalk() { return( Namespace::canTalk( $this->mNamespace ) ); } - + /** * Get the interwiki prefix (or null string) * @return string - * @access public */ - function getInterwiki() { return $this->mInterwiki; } + public function getInterwiki() { return $this->mInterwiki; } /** - * Get the Title fragment (i.e. the bit after the #) + * Get the Title fragment (i.e. the bit after the #) in text form + * @return string + */ + public function getFragment() { return $this->mFragment; } + /** + * Get the fragment in URL form, including the "#" character if there is one * @return string - * @access public */ - function getFragment() { return $this->mFragment; } + public function getFragmentForURL() { + if ( $this->mFragment == '' ) { + return ''; + } else { + return '#' . Title::escapeFragmentForURL( $this->mFragment ); + } + } /** * Get the default namespace index, for when there is no namespace * @return int - * @access public */ - function getDefaultNamespace() { return $this->mDefaultNamespace; } + public function getDefaultNamespace() { return $this->mDefaultNamespace; } /** * Get title for search index * @return string a stripped-down title string ready for the * search index */ - function getIndexTitle() { + public function getIndexTitle() { return Title::indexTitle( $this->mNamespace, $this->mTextform ); } @@ -669,9 +628,8 @@ class Title { * Get the prefixed database key form * @return string the prefixed title, with underscores and * any interwiki and namespace prefixes - * @access public */ - function getPrefixedDBkey() { + public function getPrefixedDBkey() { $s = $this->prefix( $this->mDbkeyform ); $s = str_replace( ' ', '_', $s ); return $s; @@ -681,9 +639,8 @@ class Title { * Get the prefixed title with spaces. * This is the form usually used for display * @return string the prefixed title, with spaces - * @access public */ - function getPrefixedText() { + public function getPrefixedText() { if ( empty( $this->mPrefixedText ) ) { // FIXME: bad usage of empty() ? $s = $this->prefix( $this->mTextform ); $s = str_replace( '_', ' ', $s ); @@ -697,9 +654,8 @@ class Title { * (part beginning with '#') * @return string the prefixed title, with spaces and * the fragment, including '#' - * @access public */ - function getFullText() { + public function getFullText() { $text = $this->getPrefixedText(); if( '' != $this->mFragment ) { $text .= '#' . $this->mFragment; @@ -711,7 +667,7 @@ class Title { * Get the base name, i.e. the leftmost parts before the / * @return string Base name */ - function getBaseText() { + public function getBaseText() { global $wgNamespacesWithSubpages; if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) { $parts = explode( '/', $this->getText() ); @@ -728,7 +684,7 @@ class Title { * Get the lowest-level subpage name, i.e. the rightmost part after / * @return string Subpage name */ - function getSubpageText() { + public function getSubpageText() { global $wgNamespacesWithSubpages; if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) { $parts = explode( '/', $this->mTextform ); @@ -737,12 +693,12 @@ class Title { return( $this->mTextform ); } } - + /** * Get a URL-encoded form of the subpage text * @return string URL-encoded subpage name */ - function getSubpageUrlForm() { + public function getSubpageUrlForm() { $text = $this->getSubpageText(); $text = wfUrlencode( str_replace( ' ', '_', $text ) ); $text = str_replace( '%28', '(', str_replace( '%29', ')', $text ) ); # Clean up the URL; per below, this might not be safe @@ -752,9 +708,8 @@ class Title { /** * Get a URL-encoded title (not an actual URL) including interwiki * @return string the URL-encoded form - * @access public */ - function getPrefixedURL() { + public function getPrefixedURL() { $s = $this->prefix( $this->mDbkeyform ); $s = str_replace( ' ', '_', $s ); @@ -773,14 +728,14 @@ class Title { * * @param string $query an optional query string, not used * for interwiki links + * @param string $variant language variant of url (for sr, zh..) * @return string the URL - * @access public */ - function getFullURL( $query = '' ) { + public function getFullURL( $query = '', $variant = false ) { global $wgContLang, $wgServer, $wgRequest; if ( '' == $this->mInterwiki ) { - $url = $this->getLocalUrl( $query ); + $url = $this->getLocalUrl( $query, $variant ); // Ugly quick hack to avoid duplicate prefixes (bug 4571 etc) // Correct fix would be to move the prepending elsewhere. @@ -790,26 +745,18 @@ class Title { } else { $baseUrl = $this->getInterwikiLink( $this->mInterwiki ); - $namespace = $wgContLang->getNsText( $this->mNamespace ); + $namespace = wfUrlencode( $this->getNsText() ); if ( '' != $namespace ) { # Can this actually happen? Interwikis shouldn't be parsed. + # Yes! It can in interwiki transclusion. But... it probably shouldn't. $namespace .= ':'; } $url = str_replace( '$1', $namespace . $this->mUrlform, $baseUrl ); - if( $query != '' ) { - if( false === strpos( $url, '?' ) ) { - $url .= '?'; - } else { - $url .= '&'; - } - $url .= $query; - } + $url = wfAppendQuery( $url, $query ); } # Finally, add the fragment. - if ( '' != $this->mFragment ) { - $url .= '#' . $this->mFragment; - } + $url .= $this->getFragmentForURL(); wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); return $url; @@ -820,11 +767,19 @@ class Title { * with action=render, $wgServer is prepended. * @param string $query an optional query string; if not specified, * $wgArticlePath will be used. + * @param string $variant language variant of url (for sr, zh..) * @return string the URL - * @access public */ - function getLocalURL( $query = '' ) { + public function getLocalURL( $query = '', $variant = false ) { global $wgArticlePath, $wgScript, $wgServer, $wgRequest; + global $wgVariantArticlePath, $wgContLang, $wgUser; + + // internal links should point to same variant as current page (only anonymous users) + if($variant == false && $wgContLang->hasVariants() && !$wgUser->isLoggedIn()){ + $pref = $wgContLang->getPreferredVariant(false); + if($pref != $wgContLang->getCode()) + $variant = $pref; + } if ( $this->isExternal() ) { $url = $this->getFullURL(); @@ -838,10 +793,22 @@ class Title { } else { $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); if ( $query == '' ) { - $url = str_replace( '$1', $dbkey, $wgArticlePath ); + if($variant!=false && $wgContLang->hasVariants()){ + if($wgVariantArticlePath==false) { + $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 ); + } } else { global $wgActionPaths; $url = false; + $matches = array(); if( !empty( $wgActionPaths ) && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) { @@ -876,9 +843,8 @@ class Title { * using in a link, without a server name or fragment * @param string $query an optional query string * @return string the URL - * @access public */ - function escapeLocalURL( $query = '' ) { + public function escapeLocalURL( $query = '' ) { return htmlspecialchars( $this->getLocalURL( $query ) ); } @@ -888,9 +854,8 @@ class Title { * * @return string the URL * @param string $query an optional query string - * @access public */ - function escapeFullURL( $query = '' ) { + public function escapeFullURL( $query = '' ) { return htmlspecialchars( $this->getFullURL( $query ) ); } @@ -900,12 +865,12 @@ class Title { * internal hostname for the server from the exposed one. * * @param string $query an optional query string + * @param string $variant language variant of url (for sr, zh..) * @return string the URL - * @access public */ - function getInternalURL( $query = '' ) { + public function getInternalURL( $query = '', $variant = false ) { global $wgInternalServer; - $url = $wgInternalServer . $this->getLocalURL( $query ); + $url = $wgInternalServer . $this->getLocalURL( $query, $variant ); wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); return $url; } @@ -914,9 +879,8 @@ class Title { * Get the edit URL for this Title * @return string the URL, or a null string if this is an * interwiki link - * @access public */ - function getEditURL() { + public function getEditURL() { if ( '' != $this->mInterwiki ) { return ''; } $s = $this->getLocalURL( 'action=edit' ); @@ -927,18 +891,16 @@ class Title { * Get the HTML-escaped displayable text form. * Used for the title field in tags. * @return string the text, including any prefixes - * @access public */ - function getEscapedText() { + public function getEscapedText() { return htmlspecialchars( $this->getPrefixedText() ); } /** * Is this Title interwiki? * @return boolean - * @access public */ - function isExternal() { return ( '' != $this->mInterwiki ); } + public function isExternal() { return ( '' != $this->mInterwiki ); } /** * Is this page "semi-protected" - the *only* protection is autoconfirm? @@ -946,15 +908,23 @@ class Title { * @param string Action to check (default: edit) * @return bool */ - function isSemiProtected( $action = 'edit' ) { - $restrictions = $this->getRestrictions( $action ); - # We do a full compare because this could be an array - foreach( $restrictions as $restriction ) { - if( strtolower( $restriction ) != 'autoconfirmed' ) { - return( false ); + 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; } - return( true ); } /** @@ -962,12 +932,15 @@ class Title { * @param string $what the action the page is protected from, * by default checks move and edit * @return boolean - * @access public */ - function isProtected( $action = '' ) { + public function isProtected( $action = '' ) { global $wgRestrictionLevels; - if ( -1 == $this->mNamespace ) { return true; } - + + # Special pages have inherent protection + if( $this->getNamespace() == NS_SPECIAL ) + return true; + + # Check regular protection levels if( $action == 'edit' || $action == '' ) { $r = $this->getRestrictions( 'edit' ); foreach( $wgRestrictionLevels as $level ) { @@ -992,13 +965,12 @@ class Title { /** * Is $wgUser is watching this page? * @return boolean - * @access public */ - function userIsWatching() { + public function userIsWatching() { global $wgUser; if ( is_null( $this->mWatched ) ) { - if ( -1 == $this->mNamespace || 0 == $wgUser->getID()) { + if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn()) { $this->mWatched = false; } else { $this->mWatched = $wgUser->isWatched( $this ); @@ -1008,16 +980,32 @@ class Title { } /** - * Can $wgUser perform $action this page? + * Can $wgUser perform $action on this page? + * This skips potentially expensive cascading permission checks. + * + * Suitable for use for nonessential UI controls in common cases, but + * _not_ for functional access control. + * + * May provide false positives, but should never provide a false negative. + * + * @param string $action action that permission needs to be checked for + * @return boolean + */ + public function quickUserCan( $action ) { + return $this->userCan( $action, false ); + } + + /** + * Can $wgUser perform $action on this page? * @param string $action action that permission needs to be checked for + * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. * @return boolean - * @private */ - function userCan($action) { + public function userCan( $action, $doExpensiveQueries = true ) { $fname = 'Title::userCan'; wfProfileIn( $fname ); - global $wgUser; + global $wgUser, $wgNamespaceProtection; $result = null; wfRunHooks( 'userCan', array( &$this, &$wgUser, $action, &$result ) ); @@ -1030,12 +1018,16 @@ class Title { wfProfileOut( $fname ); return false; } - // XXX: This is the code that prevents unprotecting a page in NS_MEDIAWIKI - // from taking effect -ævar - if( NS_MEDIAWIKI == $this->mNamespace && - !$wgUser->isAllowed('editinterface') ) { - wfProfileOut( $fname ); - return false; + + if ( array_key_exists( $this->mNamespace, $wgNamespaceProtection ) ) { + $nsProt = $wgNamespaceProtection[ $this->mNamespace ]; + if ( !is_array($nsProt) ) $nsProt = array($nsProt); + foreach( $nsProt as $right ) { + if( '' != $right && !$wgUser->isAllowed( $right ) ) { + wfProfileOut( $fname ); + return false; + } + } } if( $this->mDbkeyform == '_' ) { @@ -1047,14 +1039,34 @@ class Title { # protect css/js subpages of user pages # XXX: this might be better using restrictions # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working - if( NS_USER == $this->mNamespace - && preg_match("/\\.(css|js)$/", $this->mTextform ) + if( $this->isCssJsSubpage() && !$wgUser->isAllowed('editinterface') && !preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ) { wfProfileOut( $fname ); return false; } - + + if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { + # We /could/ use the protection level on the source page, but it's fairly ugly + # as we have to establish a precedence hierarchy for pages included by multiple + # cascade-protected pages. So just restrict it to people with 'protect' permission, + # as they could remove the protection anyway. + list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); + # Cascading protection depends on more than this page... + # Several cascading protected pages may include this page... + # Check each cascading level + # This is only for protection restrictions, not for all actions + if( $cascadingSources > 0 && isset($restrictions[$action]) ) { + foreach( $restrictions[$action] as $right ) { + $right = ( $right == 'sysop' ) ? 'protect' : $right; + if( '' != $right && !$wgUser->isAllowed( $right ) ) { + wfProfileOut( $fname ); + return false; + } + } + } + } + foreach( $this->getRestrictions($action) as $right ) { // Backwards compatibility, rewrite sysop -> protect if ( $right == 'sysop' ) { @@ -1075,6 +1087,7 @@ class Title { if( $action == 'create' ) { if( ( $this->isTalkPage() && !$wgUser->isAllowed( 'createtalk' ) ) || ( !$this->isTalkPage() && !$wgUser->isAllowed( 'createpage' ) ) ) { + wfProfileOut( $fname ); return false; } } @@ -1086,28 +1099,28 @@ class Title { /** * Can $wgUser edit this page? * @return boolean - * @access public + * @deprecated use userCan('edit') */ - function userCanEdit() { - return $this->userCan('edit'); + public function userCanEdit( $doExpensiveQueries = true ) { + return $this->userCan( 'edit', $doExpensiveQueries ); } /** * Can $wgUser create this page? * @return boolean - * @access public + * @deprecated use userCan('create') */ - function userCanCreate() { - return $this->userCan('create'); + public function userCanCreate( $doExpensiveQueries = true ) { + return $this->userCan( 'create', $doExpensiveQueries ); } /** * Can $wgUser move this page? * @return boolean - * @access public + * @deprecated use userCan('move') */ - function userCanMove() { - return $this->userCan('move'); + public function userCanMove( $doExpensiveQueries = true ) { + return $this->userCan( 'move', $doExpensiveQueries ); } /** @@ -1115,9 +1128,8 @@ class Title { * Some pages just aren't movable. * * @return boolean - * @access public */ - function isMovable() { + public function isMovable() { return Namespace::isMovable( $this->getNamespace() ) && $this->getInterwiki() == ''; } @@ -1125,9 +1137,9 @@ class Title { /** * Can $wgUser read this page? * @return boolean - * @access public + * @todo fold these checks into userCan() */ - function userCanRead() { + public function userCanRead() { global $wgUser; $result = null; @@ -1136,31 +1148,47 @@ class Title { return $result; } - if( $wgUser->isAllowed('read') ) { + if( $wgUser->isAllowed( 'read' ) ) { return true; } else { global $wgWhitelistRead; - /** If anon users can create an account, - they need to reach the login page first! */ - if( $wgUser->isAllowed( 'createaccount' ) - && $this->getNamespace() == NS_SPECIAL - && $this->getText() == 'Userlogin' ) { + /** + * Always grant access to the login page. + * Even anons need to be able to log in. + */ + if( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) { return true; } - - /** some pages are explicitly allowed */ + + /** + * Check for explicit whitelisting + */ $name = $this->getPrefixedText(); - if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead ) ) { + if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead, true ) ) return true; - } - - # Compatibility with old settings + + /** + * Old settings might have the title prefixed with + * a colon for main-namespace pages + */ if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) { - if( in_array( ':' . $name, $wgWhitelistRead ) ) { + if( in_array( ':' . $name, $wgWhitelistRead ) ) + return true; + } + + /** + * If it's a special page, ditch the subpage bit + * and check again + */ + if( $this->getNamespace() == NS_SPECIAL ) { + $name = $this->getText(); + list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $name ); + $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); + if( in_array( $pure, $wgWhitelistRead, true ) ) return true; - } } + } return false; } @@ -1168,32 +1196,48 @@ class Title { /** * Is this a talk page of some sort? * @return bool - * @access public */ - function isTalkPage() { + public function isTalkPage() { return Namespace::isTalk( $this->getNamespace() ); } + /** + * Is this a subpage? + * @return bool + */ + public function isSubpage() { + global $wgNamespacesWithSubpages; + + if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) ) { + return ( strpos( $this->getText(), '/' ) !== false && $wgNamespacesWithSubpages[ $this->mNamespace ] == true ); + } else { + return false; + } + } + /** * Is this a .css or .js subpage of a user page? * @return bool - * @access public */ - function isCssJsSubpage() { - return ( NS_USER == $this->mNamespace and preg_match("/\\.(css|js)$/", $this->mTextform ) ); + public function isCssJsSubpage() { + return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); } /** * Is this a *valid* .css or .js subpage of a user page? * Check that the corresponding skin exists */ - function isValidCssJsSubpage() { - global $wgValidSkinNames; - return( $this->isCssJsSubpage() && array_key_exists( $this->getSkinFromCssJsSubpage(), $wgValidSkinNames ) ); + public function isValidCssJsSubpage() { + if ( $this->isCssJsSubpage() ) { + $skinNames = Skin::getSkinNames(); + return array_key_exists( $this->getSkinFromCssJsSubpage(), $skinNames ); + } else { + return false; + } } /** * Trim down a .css or .js subpage title to get the corresponding skin name */ - function getSkinFromCssJsSubpage() { + public function getSkinFromCssJsSubpage() { $subpage = explode( '/', $this->mTextform ); $subpage = $subpage[ count( $subpage ) - 1 ]; return( str_replace( array( '.css', '.js' ), array( '', '' ), $subpage ) ); @@ -1201,18 +1245,16 @@ class Title { /** * Is this a .css subpage of a user page? * @return bool - * @access public */ - function isCssSubpage() { - return ( NS_USER == $this->mNamespace and preg_match("/\\.css$/", $this->mTextform ) ); + public function isCssSubpage() { + return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.css$/", $this->mTextform ) ); } /** * Is this a .js subpage of a user page? * @return bool - * @access public */ - function isJsSubpage() { - return ( NS_USER == $this->mNamespace and preg_match("/\\.js$/", $this->mTextform ) ); + public function isJsSubpage() { + return ( NS_USER == $this->mNamespace and preg_match("/\\/.*\\.js$/", $this->mTextform ) ); } /** * Protect css/js subpages of user pages: can $wgUser edit @@ -1220,64 +1262,242 @@ class Title { * * @return boolean * @todo XXX: this might be better using restrictions - * @access public */ - function userCanEditCssJsSubpage() { + public function userCanEditCssJsSubpage() { global $wgUser; return ( $wgUser->isAllowed('editinterface') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); } /** - * Loads a string into mRestrictions array - * @param string $res restrictions in string format - * @access public + * Cascading protection: Return true if cascading restrictions apply to this page, false if not. + * + * @return bool If the page is subject to cascading restrictions. */ - function loadRestrictions( $res ) { - foreach( explode( ':', trim( $res ) ) as $restrict ) { - $temp = explode( '=', trim( $restrict ) ); - if(count($temp) == 1) { - // old format should be treated as edit/move restriction - $this->mRestrictions["edit"] = explode( ',', trim( $temp[0] ) ); - $this->mRestrictions["move"] = explode( ',', trim( $temp[0] ) ); + public function isCascadeProtected() { + list( $sources, $restrictions ) = $this->getCascadeProtectionSources( false ); + return ( $sources > 0 ); + } + + /** + * Cascading protection: Get the source of any cascading restrictions on this page. + * + * @param $get_pages bool Whether or not to retrieve the actual pages that the restrictions have come from. + * @return array( mixed title array, restriction array) + * Array of the Title objects of the pages from which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set. + * The restriction array is an array of each type, each of which contains an array of unique groups + */ + public function getCascadeProtectionSources( $get_pages = true ) { + global $wgEnableCascadingProtection, $wgRestrictionTypes; + + # Define our dimension of restrictions types + $pagerestrictions = array(); + foreach( $wgRestrictionTypes as $action ) + $pagerestrictions[$action] = array(); + + if (!$wgEnableCascadingProtection) + return array( false, $pagerestrictions ); + + if ( isset( $this->mCascadeSources ) && $get_pages ) { + return array( $this->mCascadeSources, $this->mCascadingRestrictions ); + } else if ( isset( $this->mHasCascadingRestrictions ) && !$get_pages ) { + return array( $this->mHasCascadingRestrictions, $pagerestrictions ); + } + + wfProfileIn( __METHOD__ ); + + $dbr = wfGetDb( DB_SLAVE ); + + if ( $this->getNamespace() == NS_IMAGE ) { + $tables = array ('imagelinks', 'page_restrictions'); + $where_clauses = array( + 'il_to' => $this->getDBkey(), + 'il_from=pr_page', + 'pr_cascade' => 1 ); + } else { + $tables = array ('templatelinks', 'page_restrictions'); + $where_clauses = array( + 'tl_namespace' => $this->getNamespace(), + 'tl_title' => $this->getDBkey(), + 'tl_from=pr_page', + 'pr_cascade' => 1 ); + } + + if ( $get_pages ) { + $cols = array('pr_page', 'page_namespace', 'page_title', 'pr_expiry', 'pr_type', 'pr_level' ); + $where_clauses[] = 'page_id=pr_page'; + $tables[] = 'page'; + } else { + $cols = array( 'pr_expiry' ); + } + + $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); + + $sources = $get_pages ? array() : false; + $now = wfTimestampNow(); + $purgeExpired = false; + + while( $row = $dbr->fetchObject( $res ) ) { + $expiry = Block::decodeExpiry( $row->pr_expiry ); + if( $expiry > $now ) { + if ($get_pages) { + $page_id = $row->pr_page; + $page_ns = $row->page_namespace; + $page_title = $row->page_title; + $sources[$page_id] = Title::makeTitle($page_ns, $page_title); + # Add groups needed for each restriction type if its not already there + # Make sure this restriction type still exists + if ( isset($pagerestrictions[$row->pr_type]) && !in_array($row->pr_level, $pagerestrictions[$row->pr_type]) ) { + $pagerestrictions[$row->pr_type][]=$row->pr_level; + } + } else { + $sources = true; + } } else { - $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) ); + // Trigger lazy purge of expired restrictions from the db + $purgeExpired = true; + } + } + if( $purgeExpired ) { + Title::purgeExpiredRestrictions(); + } + + wfProfileOut( __METHOD__ ); + + if ( $get_pages ) { + $this->mCascadeSources = $sources; + $this->mCascadingRestrictions = $pagerestrictions; + } else { + $this->mHasCascadingRestrictions = $sources; + } + + return array( $sources, $pagerestrictions ); + } + + function areRestrictionsCascading() { + if (!$this->mRestrictionsLoaded) { + $this->loadRestrictions(); + } + + return $this->mCascadeRestriction; + } + + /** + * Loads a string into mRestrictions array + * @param resource $res restrictions as an SQL result. + */ + private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) { + $dbr = wfGetDb( DB_SLAVE ); + + $this->mRestrictions['edit'] = array(); + $this->mRestrictions['move'] = array(); + + # Backwards-compatibility: also load the restrictions from the page record (old format). + + if ( $oldFashionedRestrictions == NULL ) { + $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', array( 'page_id' => $this->getArticleId() ), __METHOD__ ); + } + + if ($oldFashionedRestrictions != '') { + + foreach( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { + $temp = explode( '=', trim( $restrict ) ); + if(count($temp) == 1) { + // old old format should be treated as edit/move restriction + $this->mRestrictions["edit"] = explode( ',', trim( $temp[0] ) ); + $this->mRestrictions["move"] = explode( ',', trim( $temp[0] ) ); + } else { + $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) ); + } + } + + $this->mOldRestrictions = true; + $this->mCascadeRestriction = false; + $this->mRestrictionsExpiry = Block::decodeExpiry(''); + + } + + if( $dbr->numRows( $res ) ) { + # Current system - load second to make them override. + $now = wfTimestampNow(); + $purgeExpired = false; + + while ($row = $dbr->fetchObject( $res ) ) { + # Cycle through all the restrictions. + + // This code should be refactored, now that it's being used more generally, + // But I don't really see any harm in leaving it in Block for now -werdna + $expiry = Block::decodeExpiry( $row->pr_expiry ); + + // Only apply the restrictions if they haven't expired! + if ( !$expiry || $expiry > $now ) { + $this->mRestrictionsExpiry = $expiry; + $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); + + $this->mCascadeRestriction |= $row->pr_cascade; + } else { + // Trigger a lazy purge of expired restrictions + $purgeExpired = true; + } + } + + if( $purgeExpired ) { + Title::purgeExpiredRestrictions(); } } + $this->mRestrictionsLoaded = true; } + public function loadRestrictions( $oldFashionedRestrictions = NULL ) { + if( !$this->mRestrictionsLoaded ) { + $dbr = wfGetDB( DB_SLAVE ); + + $res = $dbr->select( 'page_restrictions', '*', + array ( 'pr_page' => $this->getArticleId() ), __METHOD__ ); + + $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions ); + } + } + + /** + * Purge expired restrictions from the page_restrictions table + */ + static function purgeExpiredRestrictions() { + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'page_restrictions', + array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), + __METHOD__ ); + } + /** * Accessor/initialisation for mRestrictions + * * @param string $action action that permission needs to be checked for * @return array the array of groups allowed to edit this article - * @access public */ - function getRestrictions($action) { - $id = $this->getArticleID(); - if ( 0 == $id ) { return array(); } - - if ( ! $this->mRestrictionsLoaded ) { - $dbr =& wfGetDB( DB_SLAVE ); - $res = $dbr->selectField( 'page', 'page_restrictions', 'page_id='.$id ); - $this->loadRestrictions( $res ); - } - if( isset( $this->mRestrictions[$action] ) ) { - return $this->mRestrictions[$action]; + public function getRestrictions( $action ) { + if( $this->exists() ) { + if( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + return isset( $this->mRestrictions[$action] ) + ? $this->mRestrictions[$action] + : array(); + } else { + return array(); } - return array(); } /** * Is there a version of this page in the deletion archive? * @return int the number of archived revisions - * @access public */ - function isDeleted() { + public function isDeleted() { $fname = 'Title::isDeleted'; if ( $this->getNamespace() < 0 ) { $n = 0; } else { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), $fname ); if( $this->getNamespace() == NS_IMAGE ) { @@ -1294,9 +1514,8 @@ class Title { * @param int $flags a bit field; may be GAID_FOR_UPDATE to select * for update * @return int the ID - * @access public */ - function getArticleID( $flags = 0 ) { + public function getArticleID( $flags = 0 ) { $linkCache =& LinkCache::singleton(); if ( $flags & GAID_FOR_UPDATE ) { $oldUpdate = $linkCache->forUpdate( true ); @@ -1310,11 +1529,11 @@ class Title { return $this->mArticleID; } - function getLatestRevID() { + public function getLatestRevID() { if ($this->mLatestID !== false) return $this->mLatestID; - $db =& wfGetDB(DB_SLAVE); + $db = wfGetDB(DB_SLAVE); return $this->mLatestID = $db->selectField( 'revision', "max(rev_id)", array('rev_page' => $this->getArticleID()), @@ -1330,9 +1549,8 @@ class Title { * Article::doDeleteArticle() * * @param int $newid the new Article ID - * @access public */ - function resetArticleID( $newid ) { + public function resetArticleID( $newid ) { $linkCache =& LinkCache::singleton(); $linkCache->clearBadLink( $this->getPrefixedDBkey() ); @@ -1345,16 +1563,15 @@ class Title { /** * Updates page_touched for this page; called from LinksUpdate.php * @return bool true if the update succeded - * @access public */ - function invalidateCache() { + public function invalidateCache() { global $wgUseFileCache; if ( wfReadOnly() ) { return; } - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $success = $dbw->update( 'page', array( /* SET */ 'page_touched' => $dbw->timestamp() @@ -1365,7 +1582,7 @@ class Title { ); if ($wgUseFileCache) { - $cache = new CacheManager($this); + $cache = new HTMLFileCache($this); @unlink($cache->fileCacheName()); } @@ -1381,14 +1598,12 @@ class Title { * @private */ /* private */ function prefix( $name ) { - global $wgContLang; - $p = ''; if ( '' != $this->mInterwiki ) { $p = $this->mInterwiki . ':'; } if ( 0 != $this->mNamespace ) { - $p .= $wgContLang->getNsText( $this->mNamespace ) . ':'; + $p .= $this->getNsText() . ':'; } return $p . $name; } @@ -1402,11 +1617,9 @@ class Title { * namespace prefixes, sets the other forms, and canonicalizes * everything. * @return bool true on success - * @private */ - /* private */ function secureAndSplit() { + private function secureAndSplit() { global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks; - $fname = 'Title::secureAndSplit'; # Initialisation static $rxTc = false; @@ -1417,43 +1630,48 @@ class Title { $this->mInterwiki = $this->mFragment = ''; $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN + + $dbkey = $this->mDbkeyform; + # Strip Unicode bidi override characters. + # Sometimes they slip into cut-n-pasted page titles, where the + # override chars get included in list displays. + $dbkey = str_replace( "\xE2\x80\x8E", '', $dbkey ); // 200E LEFT-TO-RIGHT MARK + $dbkey = str_replace( "\xE2\x80\x8F", '', $dbkey ); // 200F RIGHT-TO-LEFT MARK + # Clean up whitespace # - $t = preg_replace( '/[ _]+/', '_', $this->mDbkeyform ); - $t = trim( $t, '_' ); + $dbkey = preg_replace( '/[ _]+/', '_', $dbkey ); + $dbkey = trim( $dbkey, '_' ); - if ( '' == $t ) { + if ( '' == $dbkey ) { return false; } - if( false !== strpos( $t, UTF8_REPLACEMENT ) ) { + if( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) { # Contained illegal UTF-8 sequences or forbidden Unicode chars. return false; } - $this->mDbkeyform = $t; + $this->mDbkeyform = $dbkey; # Initial colon indicates main namespace rather than specified default # but should not create invalid {ns,title} pairs such as {0,Project:Foo} - if ( ':' == $t{0} ) { + if ( ':' == $dbkey{0} ) { $this->mNamespace = NS_MAIN; - $t = substr( $t, 1 ); # remove the colon but continue processing + $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing + $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace } # Namespace or interwiki prefix $firstPass = true; do { - if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $t, $m ) ) { + $m = array(); + if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $dbkey, $m ) ) { $p = $m[1]; - $lowerNs = strtolower( $p ); - if ( $ns = Namespace::getCanonicalIndex( $lowerNs ) ) { - # Canonical namespace - $t = $m[2]; - $this->mNamespace = $ns; - } elseif ( $ns = $wgContLang->getNsIndex( $lowerNs )) { + if ( $ns = $wgContLang->getNsIndex( $p )) { # Ordinary namespace - $t = $m[2]; + $dbkey = $m[2]; $this->mNamespace = $ns; } elseif( $this->getInterwikiLink( $p ) ) { if( !$firstPass ) { @@ -1463,12 +1681,12 @@ class Title { } # Interwiki link - $t = $m[2]; - $this->mInterwiki = strtolower( $p ); + $dbkey = $m[2]; + $this->mInterwiki = $wgContLang->lc( $p ); # Redundant interwiki prefix to the local wiki if ( 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) { - if( $t == '' ) { + if( $dbkey == '' ) { # Can't have an empty self-link return false; } @@ -1480,9 +1698,9 @@ class Title { # If there's an initial colon after the interwiki, that also # resets the default namespace - if ( $t !== '' && $t[0] == ':' ) { + if ( $dbkey !== '' && $dbkey[0] == ':' ) { $this->mNamespace = NS_MAIN; - $t = substr( $t, 1 ); + $dbkey = substr( $dbkey, 1 ); } } # If there's no recognized interwiki or namespace, @@ -1490,25 +1708,24 @@ class Title { } break; } while( true ); - $r = $t; # We already know that some pages won't be in the database! # - if ( '' != $this->mInterwiki || -1 == $this->mNamespace ) { + if ( '' != $this->mInterwiki || NS_SPECIAL == $this->mNamespace ) { $this->mArticleID = 0; } - $f = strstr( $r, '#' ); - if ( false !== $f ) { - $this->mFragment = substr( $f, 1 ); - $r = substr( $r, 0, strlen( $r ) - strlen( $f ) ); + $fragment = strstr( $dbkey, '#' ); + if ( false !== $fragment ) { + $this->setFragment( $fragment ); + $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) ); # remove whitespace again: prevents "Foo_bar_#" # becoming "Foo_bar_" - $r = preg_replace( '/_*$/', '', $r ); + $dbkey = preg_replace( '/_*$/', '', $dbkey ); } # Reject illegal characters. # - if( preg_match( $rxTc, $r ) ) { + if( preg_match( $rxTc, $dbkey ) ) { return false; } @@ -1517,19 +1734,33 @@ class Title { * often be unreachable due to the way web browsers deal * with 'relative' URLs. Forbid them explicitly. */ - if ( strpos( $r, '.' ) !== false && - ( $r === '.' || $r === '..' || - strpos( $r, './' ) === 0 || - strpos( $r, '../' ) === 0 || - strpos( $r, '/./' ) !== false || - strpos( $r, '/../' ) !== false ) ) + if ( strpos( $dbkey, '.' ) !== false && + ( $dbkey === '.' || $dbkey === '..' || + strpos( $dbkey, './' ) === 0 || + strpos( $dbkey, '../' ) === 0 || + strpos( $dbkey, '/./' ) !== false || + strpos( $dbkey, '/../' ) !== false ) ) { return false; } + + /** + * Magic tilde sequences? Nu-uh! + */ + if( strpos( $dbkey, '~~~' ) !== false ) { + return false; + } - # We shouldn't need to query the DB for the size. - #$maxSize = $dbr->textFieldSize( 'page', 'page_title' ); - if ( strlen( $r ) > 255 ) { + /** + * Limit the size of titles to 255 bytes. + * This is typically the size of the underlying database field. + * We make an exception for special pages, which don't need to be stored + * in the database, and may edge over 255 bytes due to subpage syntax + * for long titles, e.g. [[Special:Block/Long name]] + */ + if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) || + strlen( $dbkey ) > 512 ) + { return false; } @@ -1541,10 +1772,9 @@ class Title { * Don't force it for interwikis, since the other * site might be case-sensitive. */ + $this->mUserCaseDBKey = $dbkey; if( $wgCapitalLinks && $this->mInterwiki == '') { - $t = $wgContLang->ucfirst( $r ); - } else { - $t = $r; + $dbkey = $wgContLang->ucfirst( $dbkey ); } /** @@ -1552,32 +1782,44 @@ class Title { * "empty" local links can only be self-links * with a fragment identifier. */ - if( $t == '' && + if( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) { return false; } // Any remaining initial :s are illegal. - if ( $t !== '' && ':' == $t{0} ) { + if ( $dbkey !== '' && ':' == $dbkey{0} ) { return false; } # Fill fields - $this->mDbkeyform = $t; - $this->mUrlform = wfUrlencode( $t ); + $this->mDbkeyform = $dbkey; + $this->mUrlform = wfUrlencode( $dbkey ); - $this->mTextform = str_replace( '_', ' ', $t ); + $this->mTextform = str_replace( '_', ' ', $dbkey ); return true; } + /** + * Set the fragment for this title + * This is kind of bad, since except for this rarely-used function, Title objects + * are immutable. The reason this is here is because it's better than setting the + * members directly, which is what Linker::formatComment was doing previously. + * + * @param string $fragment text + * @todo clarify whether access is supposed to be public (was marked as "kind of public") + */ + public function setFragment( $fragment ) { + $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); + } + /** * Get a Title object associated with the talk page of this article * @return Title the object for the talk page - * @access public */ - function getTalkPage() { + public function getTalkPage() { return Title::makeTitle( Namespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); } @@ -1586,9 +1828,8 @@ class Title { * talk page * * @return Title the object for the subject page - * @access public */ - function getSubjectPage() { + public function getSubjectPage() { return Title::makeTitle( Namespace::getSubject( $this->getNamespace() ), $this->getDBkey() ); } @@ -1601,16 +1842,14 @@ class Title { * * @param string $options may be FOR UPDATE * @return array the Title objects linking here - * @access public */ - function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) { + public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) { $linkCache =& LinkCache::singleton(); - $id = $this->getArticleID(); if ( $options ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); } $res = $db->select( array( 'page', $table ), @@ -1644,9 +1883,8 @@ class Title { * * @param string $options may be FOR UPDATE * @return array the Title objects linking here - * @access public */ - function getTemplateLinksTo( $options = '' ) { + public function getTemplateLinksTo( $options = '' ) { return $this->getLinksTo( $options, 'templatelinks', 'tl' ); } @@ -1655,13 +1893,12 @@ class Title { * * @param string $options may be FOR UPDATE * @return array the Title objects - * @access public */ - function getBrokenLinksFrom( $options = '' ) { + public function getBrokenLinksFrom( $options = '' ) { if ( $options ) { - $db =& wfGetDB( DB_MASTER ); + $db = wfGetDB( DB_MASTER ); } else { - $db =& wfGetDB( DB_SLAVE ); + $db = wfGetDB( DB_SLAVE ); } $res = $db->safeQuery( @@ -1694,16 +1931,28 @@ class Title { * page changes * * @return array the URLs - * @access public */ - function getSquidURLs() { - return array( + public function getSquidURLs() { + global $wgContLang; + + $urls = array( $this->getInternalURL(), $this->getInternalURL( 'action=history' ) ); + + // purge variant urls as well + if($wgContLang->hasVariants()){ + $variants = $wgContLang->getVariants(); + foreach($variants as $vCode){ + if($vCode==$wgContLang->getCode()) continue; // we don't want default variant + $urls[] = $this->getInternalURL('',$vCode); + } + } + + return $urls; } - function purgeSquid() { + public function purgeSquid() { global $wgUseSquid; if ( $wgUseSquid ) { $urls = $this->getSquidURLs(); @@ -1715,9 +1964,8 @@ class Title { /** * Move this page without authentication * @param Title &$nt the new page Title - * @access public */ - function moveNoAuth( &$nt ) { + public function moveNoAuth( &$nt ) { return $this->moveTo( $nt, false ); } @@ -1729,9 +1977,8 @@ class Title { * @param bool $auth indicates whether $wgUser's permissions * should be checked * @return mixed true on success, message name on failure - * @access public */ - function isValidMoveOperation( &$nt, $auth = true ) { + public function isValidMoveOperation( &$nt, $auth = true ) { if( !$this or !$nt ) { return 'badtitletext'; } @@ -1755,8 +2002,8 @@ class Title { } if ( $auth && ( - !$this->userCanEdit() || !$nt->userCanEdit() || - !$this->userCanMove() || !$nt->userCanMove() ) ) { + !$this->userCan( 'edit' ) || !$nt->userCan( 'edit' ) || + !$this->userCan( 'move' ) || !$nt->userCan( 'move' ) ) ) { return 'protectedpage'; } @@ -1778,9 +2025,8 @@ class Title { * @param bool $auth indicates whether $wgUser's permissions * should be checked * @return mixed true on success, message name on failure - * @access public */ - function moveTo( &$nt, $auth = true, $reason = '' ) { + public function moveTo( &$nt, $auth = true, $reason = '' ) { $err = $this->isValidMoveOperation( $nt, $auth ); if( is_string( $err ) ) { return $err; @@ -1797,7 +2043,7 @@ class Title { $redirid = $this->getArticleID(); # Fixing category links (those without piped 'alternate' names) to be sorted under the new title - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $categorylinks = $dbw->tableName( 'categorylinks' ); $sql = "UPDATE $categorylinks SET cl_sortkey=" . $dbw->addQuotes( $nt->getPrefixedText() ) . " WHERE cl_from=" . $dbw->addQuotes( $pageid ) . @@ -1822,24 +2068,24 @@ class Title { $u->doUpdate(); # Update site_stats - if ( $this->getNamespace() == NS_MAIN and $nt->getNamespace() != NS_MAIN ) { - # Moved out of main namespace - # not viewed, edited, removing - $u = new SiteStatsUpdate( 0, 1, -1, $pageCountChange); - } elseif ( $this->getNamespace() != NS_MAIN and $nt->getNamespace() == NS_MAIN ) { - # Moved into main namespace - # not viewed, edited, adding + 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 ) { - # Added redirect + } elseif( $pageCountChange ) { + # Redirect added $u = new SiteStatsUpdate( 0, 0, 0, 1 ); - } else{ + } else { + # Nothing special $u = false; } - if ( $u ) { + if( $u ) $u->doUpdate(); - } - + global $wgUser; wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); return true; @@ -1851,22 +2097,20 @@ class Title { * * @param Title &$nt the page to move to, which should currently * be a redirect - * @private */ - function moveOverExistingRedirect( &$nt, $reason = '' ) { - global $wgUseSquid, $wgMwRedir; + private function moveOverExistingRedirect( &$nt, $reason = '' ) { + global $wgUseSquid; $fname = 'Title::moveOverExistingRedirect'; - $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); + $comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() ); if ( $reason ) { $comment .= ": $reason"; } $now = wfTimestampNow(); - $rand = wfRandom(); $newid = $nt->getArticleID(); $oldid = $this->getArticleID(); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $linkCache =& LinkCache::singleton(); # Delete the old redirect. We don't save it to history since @@ -1893,14 +2137,15 @@ class Title { $linkCache->clearLink( $nt->getPrefixedDBkey() ); # Recreate the redirect, this time in the other direction. - $redirectText = $wgMwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; + $mwRedir = MagicWord::get( 'redirect' ); + $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; $redirectArticle = new Article( $this ); $newid = $redirectArticle->insertOn( $dbw ); $redirectRevision = new Revision( array( 'page' => $newid, 'comment' => $comment, 'text' => $redirectText ) ); - $revid = $redirectRevision->insertOn( $dbw ); + $redirectRevision->insertOn( $dbw ); $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); $linkCache->clearLink( $this->getPrefixedDBkey() ); @@ -1929,11 +2174,9 @@ class Title { /** * Move page to non-existing title. * @param Title &$nt the new Title - * @private */ - function moveToNewTitle( &$nt, $reason = '' ) { + private function moveToNewTitle( &$nt, $reason = '' ) { global $wgUseSquid; - global $wgMwRedir; $fname = 'MovePageForm::moveToNewTitle'; $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); if ( $reason ) { @@ -1942,9 +2185,8 @@ class Title { $newid = $nt->getArticleID(); $oldid = $this->getArticleID(); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $now = $dbw->timestamp(); - $rand = wfRandom(); $linkCache =& LinkCache::singleton(); # Save a null revision in the page's history notifying of the move @@ -1966,14 +2208,15 @@ class Title { $linkCache->clearLink( $nt->getPrefixedDBkey() ); # Insert redirect - $redirectText = $wgMwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; + $mwRedir = MagicWord::get( 'redirect' ); + $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; $redirectArticle = new Article( $this ); $newid = $redirectArticle->insertOn( $dbw ); $redirectRevision = new Revision( array( 'page' => $newid, 'comment' => $comment, 'text' => $redirectText ) ); - $revid = $redirectRevision->insertOn( $dbw ); + $redirectRevision->insertOn( $dbw ); $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); $linkCache->clearLink( $this->getPrefixedDBkey() ); @@ -2002,12 +2245,11 @@ class Title { * - Selects for update, so don't call it unless you mean business * * @param Title &$nt the new title to check - * @access public */ - function isValidMoveTarget( $nt ) { + public function isValidMoveTarget( $nt ) { $fname = 'Title::isValidMoveTarget'; - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); # Is it a redirect? $id = $nt->getArticleID(); @@ -2025,6 +2267,7 @@ class Title { # Does the redirect point to the source? # Or is it a broken self-redirect, usually caused by namespace collisions? + $m = array(); if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) { $redirTitle = Title::newFromText( $m[1] ); if( !is_object( $redirTitle ) || @@ -2052,59 +2295,18 @@ class Title { return $row === false; } - /** - * Create a redirect; fails if the title already exists; does - * not notify RC - * - * @param Title $dest the destination of the redirect - * @param string $comment the comment string describing the move - * @return bool true on success - * @access public - */ - function createRedirect( $dest, $comment ) { - if ( $this->getArticleID() ) { - return false; - } - - $fname = 'Title::createRedirect'; - $dbw =& wfGetDB( DB_MASTER ); - - $article = new Article( $this ); - $newid = $article->insertOn( $dbw ); - $revision = new Revision( array( - 'page' => $newid, - 'comment' => $comment, - 'text' => "#REDIRECT [[" . $dest->getPrefixedText() . "]]\n", - ) ); - $revisionId = $revision->insertOn( $dbw ); - $article->updateRevisionOn( $dbw, $revision, 0 ); - - # Link table - $dbw->insert( 'pagelinks', - array( - 'pl_from' => $newid, - 'pl_namespace' => $dest->getNamespace(), - 'pl_title' => $dest->getDbKey() - ), $fname - ); - - Article::onArticleCreate( $this ); - return true; - } - /** * Get categories to which this Title belongs and return an array of * categories' names. * * @return array an array of parents in the form: * $parent => $currentarticle - * @access public */ - function getParentCategories() { + public function getParentCategories() { global $wgContLang; $titlekey = $this->getArticleId(); - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $categorylinks = $dbr->tableName( 'categorylinks' ); # NEW SQL @@ -2130,9 +2332,8 @@ class Title { * Get a tree of parent categories * @param array $children an array with the children in the keys, to check for circular refs * @return array - * @access public */ - function getParentCategoryTree( $children = array() ) { + public function getParentCategoryTree( $children = array() ) { $parents = $this->getParentCategories(); if($parents != '') { @@ -2142,7 +2343,9 @@ class Title { $stack[$parent] = array(); } else { $nt = Title::newFromText($parent); - $stack[$parent] = $nt->getParentCategoryTree( $children + array($parent => 1) ); + if ( $nt ) { + $stack[$parent] = $nt->getParentCategoryTree( $children + array($parent => 1) ); + } } } return $stack; @@ -2157,9 +2360,8 @@ class Title { * the "page" table * * @return array - * @access public */ - function pageCond() { + public function pageCond() { return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); } @@ -2167,10 +2369,10 @@ class Title { * Get the revision ID of the previous revision * * @param integer $revision Revision ID. Get the revision that was before this one. - * @return interger $oldrevision|false + * @return integer $oldrevision|false */ - function getPreviousRevisionID( $revision ) { - $dbr =& wfGetDB( DB_SLAVE ); + public function getPreviousRevisionID( $revision ) { + $dbr = wfGetDB( DB_SLAVE ); return $dbr->selectField( 'revision', 'rev_id', 'rev_page=' . intval( $this->getArticleId() ) . ' AND rev_id<' . intval( $revision ) . ' ORDER BY rev_id DESC' ); @@ -2180,22 +2382,37 @@ class Title { * Get the revision ID of the next revision * * @param integer $revision Revision ID. Get the revision that was after this one. - * @return interger $oldrevision|false + * @return integer $oldrevision|false */ - function getNextRevisionID( $revision ) { - $dbr =& wfGetDB( DB_SLAVE ); + public function getNextRevisionID( $revision ) { + $dbr = wfGetDB( DB_SLAVE ); return $dbr->selectField( 'revision', 'rev_id', 'rev_page=' . intval( $this->getArticleId() ) . ' AND rev_id>' . intval( $revision ) . ' ORDER BY rev_id' ); } + /** + * Get the number of revisions between the given revision IDs. + * + * @param integer $old Revision ID. + * @param integer $new Revision ID. + * @return integer Number of revisions between these IDs. + */ + public function countRevisionsBetween( $old, $new ) { + $dbr = wfGetDB( DB_SLAVE ); + return $dbr->selectField( 'revision', 'count(*)', + 'rev_page = ' . intval( $this->getArticleId() ) . + ' AND rev_id > ' . intval( $old ) . + ' AND rev_id < ' . intval( $new ) ); + } + /** * Compare with another title. * * @param Title $title * @return bool */ - function equals( $title ) { + public function equals( $title ) { // Note: === is necessary for proper matching of number-like titles. return $this->getInterwiki() === $title->getInterwiki() && $this->getNamespace() == $title->getNamespace() @@ -2206,7 +2423,7 @@ class Title { * Check if page exists * @return bool */ - function exists() { + public function exists() { return $this->getArticleId() != 0; } @@ -2214,11 +2431,13 @@ class Title { * Should a link should be displayed as a known link, just based on its title? * * Currently, a self-link with a fragment and special pages are in - * this category. Special pages never exist in the database. + * this category. System messages that have defined default values are also + * always known. */ - function isAlwaysKnown() { - return $this->isExternal() || ( 0 == $this->mNamespace && "" == $this->mDbkeyform ) - || NS_SPECIAL == $this->mNamespace; + public function isAlwaysKnown() { + return ( $this->isExternal() || + ( 0 == $this->mNamespace && "" == $this->mDbkeyform ) || + ( NS_MEDIAWIKI == $this->mNamespace && wfMsgWeirdKey( $this->mDbkeyform ) ) ); } /** @@ -2226,7 +2445,7 @@ class Title { * pages linking to this title. May be sent to the job queue depending * on the number of links. Typically called on create and delete. */ - function touchLinks() { + public function touchLinks() { $u = new HTMLCacheUpdate( $this, 'pagelinks' ); $u->doUpdate(); @@ -2236,14 +2455,28 @@ class Title { } } - function trackbackURL() { + /** + * Get the last touched timestamp + */ + public function getTouched() { + $dbr = wfGetDB( DB_SLAVE ); + $touched = $dbr->selectField( 'page', 'page_touched', + array( + 'page_namespace' => $this->getNamespace(), + 'page_title' => $this->getDBkey() + ), __METHOD__ + ); + return $touched; + } + + public function trackbackURL() { global $wgTitle, $wgScriptPath, $wgServer; return "$wgServer$wgScriptPath/trackback.php?article=" . htmlspecialchars(urlencode($wgTitle->getPrefixedDBkey())); } - function trackbackRDF() { + public function trackbackRDF() { $url = htmlspecialchars($this->getFullURL()); $title = htmlspecialchars($this->getText()); $tburl = $this->trackbackURL(); @@ -2264,7 +2497,8 @@ class Title { * Generate strings used for xml 'id' names in monobook tabs * @return string */ - function getNamespaceKey() { + public function getNamespaceKey() { + global $wgContLang; switch ($this->getNamespace()) { case NS_MAIN: case NS_TALK: @@ -2295,8 +2529,52 @@ class Title { case NS_CATEGORY_TALK: return 'nstab-category'; default: - return 'nstab-' . strtolower( $this->getSubjectNsText() ); + return 'nstab-' . $wgContLang->lc( $this->getSubjectNsText() ); + } + } + + /** + * Returns true if this title resolves to the named special page + * @param string $name The special page name + */ + public function isSpecial( $name ) { + if ( $this->getNamespace() == NS_SPECIAL ) { + list( $thisName, /* $subpage */ ) = SpecialPage::resolveAliasWithSubpage( $this->getDBkey() ); + if ( $name == $thisName ) { + return true; + } + } + return false; + } + + /** + * If the Title refers to a special page alias which is not the local default, + * returns a new Title which points to the local default. Otherwise, returns $this. + */ + public function fixSpecialName() { + if ( $this->getNamespace() == NS_SPECIAL ) { + $canonicalName = SpecialPage::resolveAlias( $this->mDbkeyform ); + if ( $canonicalName ) { + $localName = SpecialPage::getLocalNameFor( $canonicalName ); + 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 bool + */ + public function isContentPage() { + return Namespace::isContent( $this->getNamespace() ); + } + } -?> + +