MediaWiki
master
|
00001 <?php 00033 class Title { 00035 // @{ 00036 static private $titleCache = array(); 00037 // @} 00038 00044 const CACHE_MAX = 1000; 00045 00050 const GAID_FOR_UPDATE = 1; 00051 00057 // @{ 00058 00059 var $mTextform = ''; // /< Text form (spaces not underscores) of the main part 00060 var $mUrlform = ''; // /< URL-encoded form of the main part 00061 var $mDbkeyform = ''; // /< Main part with underscores 00062 var $mUserCaseDBKey; // /< DB key with the initial letter in the case specified by the user 00063 var $mNamespace = NS_MAIN; // /< Namespace index, i.e. one of the NS_xxxx constants 00064 var $mInterwiki = ''; // /< Interwiki prefix (or null string) 00065 var $mFragment; // /< Title fragment (i.e. the bit after the #) 00066 var $mArticleID = -1; // /< Article ID, fetched from the link cache on demand 00067 var $mLatestID = false; // /< ID of most recent revision 00068 var $mContentModel = false; // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants 00069 private $mEstimateRevisions; // /< Estimated number of revisions; null of not loaded 00070 var $mRestrictions = array(); // /< Array of groups allowed to edit this article 00071 var $mOldRestrictions = false; 00072 var $mCascadeRestriction; 00073 var $mCascadingRestrictions; // Caching the results of getCascadeProtectionSources 00074 var $mRestrictionsExpiry = array(); 00075 var $mHasCascadingRestrictions; 00076 var $mCascadeSources; 00077 var $mRestrictionsLoaded = false; 00078 var $mPrefixedText; 00079 var $mTitleProtection; 00080 # Don't change the following default, NS_MAIN is hardcoded in several 00081 # places. See bug 696. 00082 var $mDefaultNamespace = NS_MAIN; // /< Namespace index when there is no namespace 00083 # Zero except in {{transclusion}} tags 00084 var $mWatched = null; // /< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching() 00085 var $mLength = -1; // /< The page length, 0 for special pages 00086 var $mRedirect = null; // /< Is the article at this title a redirect? 00087 var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false 00088 var $mHasSubpage; // /< Whether a page has any subpages 00089 // @} 00090 00091 00095 /*protected*/ function __construct() { } 00096 00105 public static function newFromDBkey( $key ) { 00106 $t = new Title(); 00107 $t->mDbkeyform = $key; 00108 if ( $t->secureAndSplit() ) { 00109 return $t; 00110 } else { 00111 return null; 00112 } 00113 } 00114 00128 public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { 00129 if ( is_object( $text ) ) { 00130 throw new MWException( 'Title::newFromText given an object' ); 00131 } 00132 00141 if ( $defaultNamespace == NS_MAIN && isset( Title::$titleCache[$text] ) ) { 00142 return Title::$titleCache[$text]; 00143 } 00144 00145 # Convert things like é ā or 〗 into normalized (bug 14952) text 00146 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); 00147 00148 $t = new Title(); 00149 $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); 00150 $t->mDefaultNamespace = $defaultNamespace; 00151 00152 static $cachedcount = 0 ; 00153 if ( $t->secureAndSplit() ) { 00154 if ( $defaultNamespace == NS_MAIN ) { 00155 if ( $cachedcount >= self::CACHE_MAX ) { 00156 # Avoid memory leaks on mass operations... 00157 Title::$titleCache = array(); 00158 $cachedcount = 0; 00159 } 00160 $cachedcount++; 00161 Title::$titleCache[$text] =& $t; 00162 } 00163 return $t; 00164 } else { 00165 $ret = null; 00166 return $ret; 00167 } 00168 } 00169 00185 public static function newFromURL( $url ) { 00186 $t = new Title(); 00187 00188 # For compatibility with old buggy URLs. "+" is usually not valid in titles, 00189 # but some URLs used it as a space replacement and they still come 00190 # from some external search tools. 00191 if ( strpos( self::legalChars(), '+' ) === false ) { 00192 $url = str_replace( '+', ' ', $url ); 00193 } 00194 00195 $t->mDbkeyform = str_replace( ' ', '_', $url ); 00196 if ( $t->secureAndSplit() ) { 00197 return $t; 00198 } else { 00199 return null; 00200 } 00201 } 00202 00209 protected static function getSelectFields() { 00210 global $wgContentHandlerUseDB; 00211 00212 $fields = array( 00213 'page_namespace', 'page_title', 'page_id', 00214 'page_len', 'page_is_redirect', 'page_latest', 00215 ); 00216 00217 if ( $wgContentHandlerUseDB ) { 00218 $fields[] = 'page_content_model'; 00219 } 00220 00221 return $fields; 00222 } 00223 00231 public static function newFromID( $id, $flags = 0 ) { 00232 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 00233 $row = $db->selectRow( 00234 'page', 00235 self::getSelectFields(), 00236 array( 'page_id' => $id ), 00237 __METHOD__ 00238 ); 00239 if ( $row !== false ) { 00240 $title = Title::newFromRow( $row ); 00241 } else { 00242 $title = null; 00243 } 00244 return $title; 00245 } 00246 00253 public static function newFromIDs( $ids ) { 00254 if ( !count( $ids ) ) { 00255 return array(); 00256 } 00257 $dbr = wfGetDB( DB_SLAVE ); 00258 00259 $res = $dbr->select( 00260 'page', 00261 self::getSelectFields(), 00262 array( 'page_id' => $ids ), 00263 __METHOD__ 00264 ); 00265 00266 $titles = array(); 00267 foreach ( $res as $row ) { 00268 $titles[] = Title::newFromRow( $row ); 00269 } 00270 return $titles; 00271 } 00272 00279 public static function newFromRow( $row ) { 00280 $t = self::makeTitle( $row->page_namespace, $row->page_title ); 00281 $t->loadFromRow( $row ); 00282 return $t; 00283 } 00284 00291 public function loadFromRow( $row ) { 00292 if ( $row ) { // page found 00293 if ( isset( $row->page_id ) ) 00294 $this->mArticleID = (int)$row->page_id; 00295 if ( isset( $row->page_len ) ) 00296 $this->mLength = (int)$row->page_len; 00297 if ( isset( $row->page_is_redirect ) ) 00298 $this->mRedirect = (bool)$row->page_is_redirect; 00299 if ( isset( $row->page_latest ) ) 00300 $this->mLatestID = (int)$row->page_latest; 00301 if ( isset( $row->page_content_model ) ) 00302 $this->mContentModel = strval( $row->page_content_model ); 00303 else 00304 $this->mContentModel = false; # initialized lazily in getContentModel() 00305 } else { // page not found 00306 $this->mArticleID = 0; 00307 $this->mLength = 0; 00308 $this->mRedirect = false; 00309 $this->mLatestID = 0; 00310 $this->mContentModel = false; # initialized lazily in getContentModel() 00311 } 00312 } 00313 00327 public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { 00328 $t = new Title(); 00329 $t->mInterwiki = $interwiki; 00330 $t->mFragment = $fragment; 00331 $t->mNamespace = $ns = intval( $ns ); 00332 $t->mDbkeyform = str_replace( ' ', '_', $title ); 00333 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; 00334 $t->mUrlform = wfUrlencode( $t->mDbkeyform ); 00335 $t->mTextform = str_replace( '_', ' ', $title ); 00336 $t->mContentModel = false; # initialized lazily in getContentModel() 00337 return $t; 00338 } 00339 00351 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { 00352 if ( !MWNamespace::exists( $ns ) ) { 00353 return null; 00354 } 00355 00356 $t = new Title(); 00357 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki ); 00358 if ( $t->secureAndSplit() ) { 00359 return $t; 00360 } else { 00361 return null; 00362 } 00363 } 00364 00370 public static function newMainPage() { 00371 $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() ); 00372 // Don't give fatal errors if the message is broken 00373 if ( !$title ) { 00374 $title = Title::newFromText( 'Main Page' ); 00375 } 00376 return $title; 00377 } 00378 00389 public static function newFromRedirect( $text ) { 00390 ContentHandler::deprecated( __METHOD__, '1.21' ); 00391 00392 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00393 return $content->getRedirectTarget(); 00394 } 00395 00406 public static function newFromRedirectRecurse( $text ) { 00407 ContentHandler::deprecated( __METHOD__, '1.21' ); 00408 00409 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00410 return $content->getUltimateRedirectTarget(); 00411 } 00412 00423 public static function newFromRedirectArray( $text ) { 00424 ContentHandler::deprecated( __METHOD__, '1.21' ); 00425 00426 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00427 return $content->getRedirectChain(); 00428 } 00429 00436 public static function nameOf( $id ) { 00437 $dbr = wfGetDB( DB_SLAVE ); 00438 00439 $s = $dbr->selectRow( 00440 'page', 00441 array( 'page_namespace', 'page_title' ), 00442 array( 'page_id' => $id ), 00443 __METHOD__ 00444 ); 00445 if ( $s === false ) { 00446 return null; 00447 } 00448 00449 $n = self::makeName( $s->page_namespace, $s->page_title ); 00450 return $n; 00451 } 00452 00458 public static function legalChars() { 00459 global $wgLegalTitleChars; 00460 return $wgLegalTitleChars; 00461 } 00462 00470 static function getTitleInvalidRegex() { 00471 static $rxTc = false; 00472 if ( !$rxTc ) { 00473 # Matching titles will be held as illegal. 00474 $rxTc = '/' . 00475 # Any character not allowed is forbidden... 00476 '[^' . self::legalChars() . ']' . 00477 # URL percent encoding sequences interfere with the ability 00478 # to round-trip titles -- you can't link to them consistently. 00479 '|%[0-9A-Fa-f]{2}' . 00480 # XML/HTML character references produce similar issues. 00481 '|&[A-Za-z0-9\x80-\xff]+;' . 00482 '|&#[0-9]+;' . 00483 '|&#x[0-9A-Fa-f]+;' . 00484 '/S'; 00485 } 00486 00487 return $rxTc; 00488 } 00489 00498 public static function indexTitle( $ns, $title ) { 00499 global $wgContLang; 00500 00501 $lc = SearchEngine::legalSearchChars() . '&#;'; 00502 $t = $wgContLang->normalizeForSearch( $title ); 00503 $t = preg_replace( "/[^{$lc}]+/", ' ', $t ); 00504 $t = $wgContLang->lc( $t ); 00505 00506 # Handle 's, s' 00507 $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t ); 00508 $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t ); 00509 00510 $t = preg_replace( "/\\s+/", ' ', $t ); 00511 00512 if ( $ns == NS_FILE ) { 00513 $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t ); 00514 } 00515 return trim( $t ); 00516 } 00517 00527 public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) { 00528 global $wgContLang; 00529 00530 $namespace = $wgContLang->getNsText( $ns ); 00531 $name = $namespace == '' ? $title : "$namespace:$title"; 00532 if ( strval( $interwiki ) != '' ) { 00533 $name = "$interwiki:$name"; 00534 } 00535 if ( strval( $fragment ) != '' ) { 00536 $name .= '#' . $fragment; 00537 } 00538 return $name; 00539 } 00540 00547 static function escapeFragmentForURL( $fragment ) { 00548 # Note that we don't urlencode the fragment. urlencoded Unicode 00549 # fragments appear not to work in IE (at least up to 7) or in at least 00550 # one version of Opera 9.x. The W3C validator, for one, doesn't seem 00551 # to care if they aren't encoded. 00552 return Sanitizer::escapeId( $fragment, 'noninitial' ); 00553 } 00554 00563 public static function compare( $a, $b ) { 00564 if ( $a->getNamespace() == $b->getNamespace() ) { 00565 return strcmp( $a->getText(), $b->getText() ); 00566 } else { 00567 return $a->getNamespace() - $b->getNamespace(); 00568 } 00569 } 00570 00577 public function isLocal() { 00578 if ( $this->mInterwiki != '' ) { 00579 return Interwiki::fetch( $this->mInterwiki )->isLocal(); 00580 } else { 00581 return true; 00582 } 00583 } 00584 00590 public function isExternal() { 00591 return ( $this->mInterwiki != '' ); 00592 } 00593 00599 public function getInterwiki() { 00600 return $this->mInterwiki; 00601 } 00602 00609 public function isTrans() { 00610 if ( $this->mInterwiki == '' ) { 00611 return false; 00612 } 00613 00614 return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); 00615 } 00616 00622 public function getTransWikiID() { 00623 if ( $this->mInterwiki == '' ) { 00624 return false; 00625 } 00626 00627 return Interwiki::fetch( $this->mInterwiki )->getWikiID(); 00628 } 00629 00635 public function getText() { 00636 return $this->mTextform; 00637 } 00638 00644 public function getPartialURL() { 00645 return $this->mUrlform; 00646 } 00647 00653 public function getDBkey() { 00654 return $this->mDbkeyform; 00655 } 00656 00662 function getUserCaseDBKey() { 00663 return $this->mUserCaseDBKey; 00664 } 00665 00671 public function getNamespace() { 00672 return $this->mNamespace; 00673 } 00674 00680 public function getContentModel() { 00681 if ( !$this->mContentModel ) { 00682 $linkCache = LinkCache::singleton(); 00683 $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); 00684 } 00685 00686 if ( !$this->mContentModel ) { 00687 $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); 00688 } 00689 00690 if( !$this->mContentModel ) { 00691 throw new MWException( "failed to determin content model!" ); 00692 } 00693 00694 return $this->mContentModel; 00695 } 00696 00703 public function hasContentModel( $id ) { 00704 return $this->getContentModel() == $id; 00705 } 00706 00712 public function getNsText() { 00713 global $wgContLang; 00714 00715 if ( $this->mInterwiki != '' ) { 00716 // This probably shouldn't even happen. ohh man, oh yuck. 00717 // But for interwiki transclusion it sometimes does. 00718 // Shit. Shit shit shit. 00719 // 00720 // Use the canonical namespaces if possible to try to 00721 // resolve a foreign namespace. 00722 if ( MWNamespace::exists( $this->mNamespace ) ) { 00723 return MWNamespace::getCanonicalName( $this->mNamespace ); 00724 } 00725 } 00726 00727 if ( $wgContLang->needsGenderDistinction() && 00728 MWNamespace::hasGenderDistinction( $this->mNamespace ) ) { 00729 $gender = GenderCache::singleton()->getGenderOf( $this->getText(), __METHOD__ ); 00730 return $wgContLang->getGenderNsText( $this->mNamespace, $gender ); 00731 } 00732 00733 return $wgContLang->getNsText( $this->mNamespace ); 00734 } 00735 00741 public function getSubjectNsText() { 00742 global $wgContLang; 00743 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) ); 00744 } 00745 00751 public function getTalkNsText() { 00752 global $wgContLang; 00753 return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) ); 00754 } 00755 00761 public function canTalk() { 00762 return( MWNamespace::canTalk( $this->mNamespace ) ); 00763 } 00764 00771 public function canExist() { 00772 return $this->mNamespace >= NS_MAIN; 00773 } 00774 00780 public function isWatchable() { 00781 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); 00782 } 00783 00789 public function isSpecialPage() { 00790 return $this->getNamespace() == NS_SPECIAL; 00791 } 00792 00799 public function isSpecial( $name ) { 00800 if ( $this->isSpecialPage() ) { 00801 list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); 00802 if ( $name == $thisName ) { 00803 return true; 00804 } 00805 } 00806 return false; 00807 } 00808 00815 public function fixSpecialName() { 00816 if ( $this->isSpecialPage() ) { 00817 list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); 00818 if ( $canonicalName ) { 00819 $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); 00820 if ( $localName != $this->mDbkeyform ) { 00821 return Title::makeTitle( NS_SPECIAL, $localName ); 00822 } 00823 } 00824 } 00825 return $this; 00826 } 00827 00838 public function inNamespace( $ns ) { 00839 return MWNamespace::equals( $this->getNamespace(), $ns ); 00840 } 00841 00849 public function inNamespaces( /* ... */ ) { 00850 $namespaces = func_get_args(); 00851 if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { 00852 $namespaces = $namespaces[0]; 00853 } 00854 00855 foreach ( $namespaces as $ns ) { 00856 if ( $this->inNamespace( $ns ) ) { 00857 return true; 00858 } 00859 } 00860 00861 return false; 00862 } 00863 00877 public function hasSubjectNamespace( $ns ) { 00878 return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); 00879 } 00880 00888 public function isContentPage() { 00889 return MWNamespace::isContent( $this->getNamespace() ); 00890 } 00891 00898 public function isMovable() { 00899 if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) { 00900 // Interwiki title or immovable namespace. Hooks don't get to override here 00901 return false; 00902 } 00903 00904 $result = true; 00905 wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); 00906 return $result; 00907 } 00908 00919 public function isMainPage() { 00920 return $this->equals( Title::newMainPage() ); 00921 } 00922 00928 public function isSubpage() { 00929 return MWNamespace::hasSubpages( $this->mNamespace ) 00930 ? strpos( $this->getText(), '/' ) !== false 00931 : false; 00932 } 00933 00939 public function isConversionTable() { 00940 //@todo: ConversionTable should become a separate content model. 00941 00942 return $this->getNamespace() == NS_MEDIAWIKI && 00943 strpos( $this->getText(), 'Conversiontable/' ) === 0; 00944 } 00945 00951 public function isWikitextPage() { 00952 return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); 00953 } 00954 00966 public function isCssOrJsPage() { 00967 $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace 00968 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 00969 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 00970 00971 #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure 00972 # hook funktions can force this method to return true even outside the mediawiki namespace. 00973 00974 wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) ); 00975 00976 return $isCssOrJsPage; 00977 } 00978 00983 public function isCssJsSubpage() { 00984 return ( NS_USER == $this->mNamespace && $this->isSubpage() 00985 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 00986 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); 00987 } 00988 00994 public function getSkinFromCssJsSubpage() { 00995 $subpage = explode( '/', $this->mTextform ); 00996 $subpage = $subpage[ count( $subpage ) - 1 ]; 00997 $lastdot = strrpos( $subpage, '.' ); 00998 if ( $lastdot === false ) 00999 return $subpage; # Never happens: only called for names ending in '.css' or '.js' 01000 return substr( $subpage, 0, $lastdot ); 01001 } 01002 01008 public function isCssSubpage() { 01009 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01010 && $this->hasContentModel( CONTENT_MODEL_CSS ) ); 01011 } 01012 01018 public function isJsSubpage() { 01019 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01020 && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01021 } 01022 01028 public function isTalkPage() { 01029 return MWNamespace::isTalk( $this->getNamespace() ); 01030 } 01031 01037 public function getTalkPage() { 01038 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); 01039 } 01040 01047 public function getSubjectPage() { 01048 // Is this the same title? 01049 $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); 01050 if ( $this->getNamespace() == $subjectNS ) { 01051 return $this; 01052 } 01053 return Title::makeTitle( $subjectNS, $this->getDBkey() ); 01054 } 01055 01061 public function getDefaultNamespace() { 01062 return $this->mDefaultNamespace; 01063 } 01064 01071 public function getIndexTitle() { 01072 return Title::indexTitle( $this->mNamespace, $this->mTextform ); 01073 } 01074 01080 public function getFragment() { 01081 return $this->mFragment; 01082 } 01083 01088 public function getFragmentForURL() { 01089 if ( $this->mFragment == '' ) { 01090 return ''; 01091 } else { 01092 return '#' . Title::escapeFragmentForURL( $this->mFragment ); 01093 } 01094 } 01095 01106 public function setFragment( $fragment ) { 01107 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); 01108 } 01109 01118 private function prefix( $name ) { 01119 $p = ''; 01120 if ( $this->mInterwiki != '' ) { 01121 $p = $this->mInterwiki . ':'; 01122 } 01123 01124 if ( 0 != $this->mNamespace ) { 01125 $p .= $this->getNsText() . ':'; 01126 } 01127 return $p . $name; 01128 } 01129 01136 public function getPrefixedDBkey() { 01137 $s = $this->prefix( $this->mDbkeyform ); 01138 $s = str_replace( ' ', '_', $s ); 01139 return $s; 01140 } 01141 01148 public function getPrefixedText() { 01149 // @todo FIXME: Bad usage of empty() ? 01150 if ( empty( $this->mPrefixedText ) ) { 01151 $s = $this->prefix( $this->mTextform ); 01152 $s = str_replace( '_', ' ', $s ); 01153 $this->mPrefixedText = $s; 01154 } 01155 return $this->mPrefixedText; 01156 } 01157 01163 public function __toString() { 01164 return $this->getPrefixedText(); 01165 } 01166 01173 public function getFullText() { 01174 $text = $this->getPrefixedText(); 01175 if ( $this->mFragment != '' ) { 01176 $text .= '#' . $this->mFragment; 01177 } 01178 return $text; 01179 } 01180 01193 public function getRootText() { 01194 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01195 return $this->getText(); 01196 } 01197 01198 return strtok( $this->getText(), '/' ); 01199 } 01200 01213 public function getRootTitle() { 01214 return Title::makeTitle( $this->getNamespace(), $this->getRootText() ); 01215 } 01216 01228 public function getBaseText() { 01229 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01230 return $this->getText(); 01231 } 01232 01233 $parts = explode( '/', $this->getText() ); 01234 # Don't discard the real title if there's no subpage involved 01235 if ( count( $parts ) > 1 ) { 01236 unset( $parts[count( $parts ) - 1] ); 01237 } 01238 return implode( '/', $parts ); 01239 } 01240 01253 public function getBaseTitle() { 01254 return Title::makeTitle( $this->getNamespace(), $this->getBaseText() ); 01255 } 01256 01268 public function getSubpageText() { 01269 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01270 return( $this->mTextform ); 01271 } 01272 $parts = explode( '/', $this->mTextform ); 01273 return( $parts[count( $parts ) - 1] ); 01274 } 01275 01289 public function getSubpage( $text ) { 01290 return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text ); 01291 } 01292 01299 public function getEscapedText() { 01300 wfDeprecated( __METHOD__, '1.19' ); 01301 return htmlspecialchars( $this->getPrefixedText() ); 01302 } 01303 01309 public function getSubpageUrlForm() { 01310 $text = $this->getSubpageText(); 01311 $text = wfUrlencode( str_replace( ' ', '_', $text ) ); 01312 return( $text ); 01313 } 01314 01320 public function getPrefixedURL() { 01321 $s = $this->prefix( $this->mDbkeyform ); 01322 $s = wfUrlencode( str_replace( ' ', '_', $s ) ); 01323 return $s; 01324 } 01325 01339 private static function fixUrlQueryArgs( $query, $query2 = false ) { 01340 if( $query2 !== false ) { 01341 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" ); 01342 } 01343 if ( is_array( $query ) ) { 01344 $query = wfArrayToCGI( $query ); 01345 } 01346 if ( $query2 ) { 01347 if ( is_string( $query2 ) ) { 01348 // $query2 is a string, we will consider this to be 01349 // a deprecated $variant argument and add it to the query 01350 $query2 = wfArrayToCGI( array( 'variant' => $query2 ) ); 01351 } else { 01352 $query2 = wfArrayToCGI( $query2 ); 01353 } 01354 // If we have $query content add a & to it first 01355 if ( $query ) { 01356 $query .= '&'; 01357 } 01358 // Now append the queries together 01359 $query .= $query2; 01360 } 01361 return $query; 01362 } 01363 01377 public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01378 $query = self::fixUrlQueryArgs( $query, $query2 ); 01379 01380 # Hand off all the decisions on urls to getLocalURL 01381 $url = $this->getLocalURL( $query ); 01382 01383 # Expand the url to make it a full url. Note that getLocalURL has the 01384 # potential to output full urls for a variety of reasons, so we use 01385 # wfExpandUrl instead of simply prepending $wgServer 01386 $url = wfExpandUrl( $url, $proto ); 01387 01388 # Finally, add the fragment. 01389 $url .= $this->getFragmentForURL(); 01390 01391 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); 01392 return $url; 01393 } 01394 01413 public function getLocalURL( $query = '', $query2 = false ) { 01414 global $wgArticlePath, $wgScript, $wgServer, $wgRequest; 01415 01416 $query = self::fixUrlQueryArgs( $query, $query2 ); 01417 01418 $interwiki = Interwiki::fetch( $this->mInterwiki ); 01419 if ( $interwiki ) { 01420 $namespace = $this->getNsText(); 01421 if ( $namespace != '' ) { 01422 # Can this actually happen? Interwikis shouldn't be parsed. 01423 # Yes! It can in interwiki transclusion. But... it probably shouldn't. 01424 $namespace .= ':'; 01425 } 01426 $url = $interwiki->getURL( $namespace . $this->getDBkey() ); 01427 $url = wfAppendQuery( $url, $query ); 01428 } else { 01429 $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); 01430 if ( $query == '' ) { 01431 $url = str_replace( '$1', $dbkey, $wgArticlePath ); 01432 wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); 01433 } else { 01434 global $wgVariantArticlePath, $wgActionPaths; 01435 $url = false; 01436 $matches = array(); 01437 01438 if ( !empty( $wgActionPaths ) && 01439 preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) 01440 { 01441 $action = urldecode( $matches[2] ); 01442 if ( isset( $wgActionPaths[$action] ) ) { 01443 $query = $matches[1]; 01444 if ( isset( $matches[4] ) ) { 01445 $query .= $matches[4]; 01446 } 01447 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); 01448 if ( $query != '' ) { 01449 $url = wfAppendQuery( $url, $query ); 01450 } 01451 } 01452 } 01453 01454 if ( $url === false && 01455 $wgVariantArticlePath && 01456 $this->getPageLanguage()->hasVariants() && 01457 preg_match( '/^variant=([^&]*)$/', $query, $matches ) ) 01458 { 01459 $variant = urldecode( $matches[1] ); 01460 if ( $this->getPageLanguage()->hasVariant( $variant ) ) { 01461 // Only do the variant replacement if the given variant is a valid 01462 // variant for the page's language. 01463 $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); 01464 $url = str_replace( '$1', $dbkey, $url ); 01465 } 01466 } 01467 01468 if ( $url === false ) { 01469 if ( $query == '-' ) { 01470 $query = ''; 01471 } 01472 $url = "{$wgScript}?title={$dbkey}&{$query}"; 01473 } 01474 } 01475 01476 wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) ); 01477 01478 // @todo FIXME: This causes breakage in various places when we 01479 // actually expected a local URL and end up with dupe prefixes. 01480 if ( $wgRequest->getVal( 'action' ) == 'render' ) { 01481 $url = $wgServer . $url; 01482 } 01483 } 01484 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) ); 01485 return $url; 01486 } 01487 01506 public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01507 wfProfileIn( __METHOD__ ); 01508 if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) { 01509 $ret = $this->getFullURL( $query, $query2, $proto ); 01510 } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { 01511 $ret = $this->getFragmentForURL(); 01512 } else { 01513 $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL(); 01514 } 01515 wfProfileOut( __METHOD__ ); 01516 return $ret; 01517 } 01518 01530 public function escapeLocalURL( $query = '', $query2 = false ) { 01531 wfDeprecated( __METHOD__, '1.19' ); 01532 return htmlspecialchars( $this->getLocalURL( $query, $query2 ) ); 01533 } 01534 01544 public function escapeFullURL( $query = '', $query2 = false ) { 01545 wfDeprecated( __METHOD__, '1.19' ); 01546 return htmlspecialchars( $this->getFullURL( $query, $query2 ) ); 01547 } 01548 01563 public function getInternalURL( $query = '', $query2 = false ) { 01564 global $wgInternalServer, $wgServer; 01565 $query = self::fixUrlQueryArgs( $query, $query2 ); 01566 $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; 01567 $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP ); 01568 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); 01569 return $url; 01570 } 01571 01585 public function getCanonicalURL( $query = '', $query2 = false ) { 01586 $query = self::fixUrlQueryArgs( $query, $query2 ); 01587 $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL ); 01588 wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) ); 01589 return $url; 01590 } 01591 01601 public function escapeCanonicalURL( $query = '', $query2 = false ) { 01602 wfDeprecated( __METHOD__, '1.19' ); 01603 return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) ); 01604 } 01605 01612 public function getEditURL() { 01613 if ( $this->mInterwiki != '' ) { 01614 return ''; 01615 } 01616 $s = $this->getLocalURL( 'action=edit' ); 01617 01618 return $s; 01619 } 01620 01627 public function userIsWatching() { 01628 global $wgUser; 01629 01630 if ( is_null( $this->mWatched ) ) { 01631 if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) { 01632 $this->mWatched = false; 01633 } else { 01634 $this->mWatched = $wgUser->isWatched( $this ); 01635 } 01636 } 01637 return $this->mWatched; 01638 } 01639 01647 public function userCanRead() { 01648 wfDeprecated( __METHOD__, '1.19' ); 01649 return $this->userCan( 'read' ); 01650 } 01651 01667 public function quickUserCan( $action, $user = null ) { 01668 return $this->userCan( $action, $user, false ); 01669 } 01670 01681 public function userCan( $action, $user = null, $doExpensiveQueries = true ) { 01682 if ( !$user instanceof User ) { 01683 global $wgUser; 01684 $user = $wgUser; 01685 } 01686 return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) ); 01687 } 01688 01702 public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { 01703 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); 01704 01705 // Remove the errors being ignored. 01706 foreach ( $errors as $index => $error ) { 01707 $error_key = is_array( $error ) ? $error[0] : $error; 01708 01709 if ( in_array( $error_key, $ignoreErrors ) ) { 01710 unset( $errors[$index] ); 01711 } 01712 } 01713 01714 return $errors; 01715 } 01716 01728 private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01729 if ( $action == 'create' ) { 01730 if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || 01731 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) { 01732 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); 01733 } 01734 } elseif ( $action == 'move' ) { 01735 if ( !$user->isAllowed( 'move-rootuserpages' ) 01736 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01737 // Show user page-specific message only if the user can move other pages 01738 $errors[] = array( 'cant-move-user-page' ); 01739 } 01740 01741 // Check if user is allowed to move files if it's a file 01742 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { 01743 $errors[] = array( 'movenotallowedfile' ); 01744 } 01745 01746 if ( !$user->isAllowed( 'move' ) ) { 01747 // User can't move anything 01748 $userCanMove = User::groupHasPermission( 'user', 'move' ); 01749 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); 01750 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { 01751 // custom message if logged-in users without any special rights can move 01752 $errors[] = array( 'movenologintext' ); 01753 } else { 01754 $errors[] = array( 'movenotallowed' ); 01755 } 01756 } 01757 } elseif ( $action == 'move-target' ) { 01758 if ( !$user->isAllowed( 'move' ) ) { 01759 // User can't move anything 01760 $errors[] = array( 'movenotallowed' ); 01761 } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 01762 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01763 // Show user page-specific message only if the user can move other pages 01764 $errors[] = array( 'cant-move-to-user-page' ); 01765 } 01766 } elseif ( !$user->isAllowed( $action ) ) { 01767 $errors[] = $this->missingPermissionError( $action, $short ); 01768 } 01769 01770 return $errors; 01771 } 01772 01781 private function resultToError( $errors, $result ) { 01782 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { 01783 // A single array representing an error 01784 $errors[] = $result; 01785 } elseif ( is_array( $result ) && is_array( $result[0] ) ) { 01786 // A nested array representing multiple errors 01787 $errors = array_merge( $errors, $result ); 01788 } elseif ( $result !== '' && is_string( $result ) ) { 01789 // A string representing a message-id 01790 $errors[] = array( $result ); 01791 } elseif ( $result === false ) { 01792 // a generic "We don't want them to do that" 01793 $errors[] = array( 'badaccess-group0' ); 01794 } 01795 return $errors; 01796 } 01797 01809 private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { 01810 // Use getUserPermissionsErrors instead 01811 $result = ''; 01812 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { 01813 return $result ? array() : array( array( 'badaccess-group0' ) ); 01814 } 01815 // Check getUserPermissionsErrors hook 01816 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { 01817 $errors = $this->resultToError( $errors, $result ); 01818 } 01819 // Check getUserPermissionsErrorsExpensive hook 01820 if ( $doExpensiveQueries && !( $short && count( $errors ) > 0 ) && 01821 !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) { 01822 $errors = $this->resultToError( $errors, $result ); 01823 } 01824 01825 return $errors; 01826 } 01827 01839 private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01840 # Only 'createaccount' and 'execute' can be performed on 01841 # special pages, which don't actually exist in the DB. 01842 $specialOKActions = array( 'createaccount', 'execute', 'read' ); 01843 if ( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions ) ) { 01844 $errors[] = array( 'ns-specialprotected' ); 01845 } 01846 01847 # Check $wgNamespaceProtection for restricted namespaces 01848 if ( $this->isNamespaceProtected( $user ) ) { 01849 $ns = $this->mNamespace == NS_MAIN ? 01850 wfMessage( 'nstab-main' )->text() : $this->getNsText(); 01851 $errors[] = $this->mNamespace == NS_MEDIAWIKI ? 01852 array( 'protectedinterface' ) : array( 'namespaceprotected', $ns ); 01853 } 01854 01855 return $errors; 01856 } 01857 01869 private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01870 # Protect css/js subpages of user pages 01871 # XXX: this might be better using restrictions 01872 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only 01873 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) 01874 && !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { 01875 if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { 01876 $errors[] = array( 'customcssprotected' ); 01877 } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { 01878 $errors[] = array( 'customjsprotected' ); 01879 } 01880 } 01881 01882 return $errors; 01883 } 01884 01898 private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01899 foreach ( $this->getRestrictions( $action ) as $right ) { 01900 // Backwards compatibility, rewrite sysop -> protect 01901 if ( $right == 'sysop' ) { 01902 $right = 'protect'; 01903 } 01904 if ( $right != '' && !$user->isAllowed( $right ) ) { 01905 // Users with 'editprotected' permission can edit protected pages 01906 // without cascading option turned on. 01907 if ( $action != 'edit' || !$user->isAllowed( 'editprotected' ) 01908 || $this->mCascadeRestriction ) 01909 { 01910 $errors[] = array( 'protectedpagetext', $right ); 01911 } 01912 } 01913 } 01914 01915 return $errors; 01916 } 01917 01929 private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01930 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { 01931 # We /could/ use the protection level on the source page, but it's 01932 # fairly ugly as we have to establish a precedence hierarchy for pages 01933 # included by multiple cascade-protected pages. So just restrict 01934 # it to people with 'protect' permission, as they could remove the 01935 # protection anyway. 01936 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); 01937 # Cascading protection depends on more than this page... 01938 # Several cascading protected pages may include this page... 01939 # Check each cascading level 01940 # This is only for protection restrictions, not for all actions 01941 if ( isset( $restrictions[$action] ) ) { 01942 foreach ( $restrictions[$action] as $right ) { 01943 $right = ( $right == 'sysop' ) ? 'protect' : $right; 01944 if ( $right != '' && !$user->isAllowed( $right ) ) { 01945 $pages = ''; 01946 foreach ( $cascadingSources as $page ) 01947 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; 01948 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); 01949 } 01950 } 01951 } 01952 } 01953 01954 return $errors; 01955 } 01956 01968 private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01969 global $wgDeleteRevisionsLimit, $wgLang; 01970 01971 if ( $action == 'protect' ) { 01972 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) { 01973 // If they can't edit, they shouldn't protect. 01974 $errors[] = array( 'protect-cantedit' ); 01975 } 01976 } elseif ( $action == 'create' ) { 01977 $title_protection = $this->getTitleProtection(); 01978 if( $title_protection ) { 01979 if( $title_protection['pt_create_perm'] == 'sysop' ) { 01980 $title_protection['pt_create_perm'] = 'protect'; // B/C 01981 } 01982 if( $title_protection['pt_create_perm'] == '' || 01983 !$user->isAllowed( $title_protection['pt_create_perm'] ) ) 01984 { 01985 $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] ); 01986 } 01987 } 01988 } elseif ( $action == 'move' ) { 01989 // Check for immobile pages 01990 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 01991 // Specific message for this case 01992 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 01993 } elseif ( !$this->isMovable() ) { 01994 // Less specific message for rarer cases 01995 $errors[] = array( 'immobile-source-page' ); 01996 } 01997 } elseif ( $action == 'move-target' ) { 01998 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 01999 $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); 02000 } elseif ( !$this->isMovable() ) { 02001 $errors[] = array( 'immobile-target-page' ); 02002 } 02003 } elseif ( $action == 'delete' ) { 02004 if ( $doExpensiveQueries && $wgDeleteRevisionsLimit 02005 && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() ) 02006 { 02007 $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); 02008 } 02009 } 02010 return $errors; 02011 } 02012 02024 private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { 02025 // Account creation blocks handled at userlogin. 02026 // Unblocking handled in SpecialUnblock 02027 if( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { 02028 return $errors; 02029 } 02030 02031 global $wgContLang, $wgLang, $wgEmailConfirmToEdit; 02032 02033 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { 02034 $errors[] = array( 'confirmedittext' ); 02035 } 02036 02037 if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) { 02038 // Don't block the user from editing their own talk page unless they've been 02039 // explicitly blocked from that too. 02040 } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { 02041 $block = $user->getBlock(); 02042 02043 // This is from OutputPage::blockedPage 02044 // Copied at r23888 by werdna 02045 02046 $id = $user->blockedBy(); 02047 $reason = $user->blockedFor(); 02048 if ( $reason == '' ) { 02049 $reason = wfMessage( 'blockednoreason' )->text(); 02050 } 02051 $ip = $user->getRequest()->getIP(); 02052 02053 if ( is_numeric( $id ) ) { 02054 $name = User::whoIs( $id ); 02055 } else { 02056 $name = $id; 02057 } 02058 02059 $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; 02060 $blockid = $block->getId(); 02061 $blockExpiry = $block->getExpiry(); 02062 $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true ); 02063 if ( $blockExpiry == 'infinity' ) { 02064 $blockExpiry = wfMessage( 'infiniteblock' )->text(); 02065 } else { 02066 $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); 02067 } 02068 02069 $intended = strval( $block->getTarget() ); 02070 02071 $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name, 02072 $blockid, $blockExpiry, $intended, $blockTimestamp ); 02073 } 02074 02075 return $errors; 02076 } 02077 02089 private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02090 global $wgWhitelistRead, $wgRevokePermissions; 02091 static $useShortcut = null; 02092 02093 # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below 02094 if ( is_null( $useShortcut ) ) { 02095 $useShortcut = true; 02096 if ( !User::groupHasPermission( '*', 'read' ) ) { 02097 # Not a public wiki, so no shortcut 02098 $useShortcut = false; 02099 } elseif ( !empty( $wgRevokePermissions ) ) { 02106 foreach ( $wgRevokePermissions as $perms ) { 02107 if ( !empty( $perms['read'] ) ) { 02108 # We might be removing the read right from the user, so no shortcut 02109 $useShortcut = false; 02110 break; 02111 } 02112 } 02113 } 02114 } 02115 02116 $whitelisted = false; 02117 if ( $useShortcut ) { 02118 # Shortcut for public wikis, allows skipping quite a bit of code 02119 $whitelisted = true; 02120 } elseif ( $user->isAllowed( 'read' ) ) { 02121 # If the user is allowed to read pages, he is allowed to read all pages 02122 $whitelisted = true; 02123 } elseif ( $this->isSpecial( 'Userlogin' ) 02124 || $this->isSpecial( 'ChangePassword' ) 02125 || $this->isSpecial( 'PasswordReset' ) 02126 ) { 02127 # Always grant access to the login page. 02128 # Even anons need to be able to log in. 02129 $whitelisted = true; 02130 } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { 02131 # Time to check the whitelist 02132 # Only do these checks is there's something to check against 02133 $name = $this->getPrefixedText(); 02134 $dbName = $this->getPrefixedDBKey(); 02135 02136 // Check for explicit whitelisting with and without underscores 02137 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { 02138 $whitelisted = true; 02139 } elseif ( $this->getNamespace() == NS_MAIN ) { 02140 # Old settings might have the title prefixed with 02141 # a colon for main-namespace pages 02142 if ( in_array( ':' . $name, $wgWhitelistRead ) ) { 02143 $whitelisted = true; 02144 } 02145 } elseif ( $this->isSpecialPage() ) { 02146 # If it's a special page, ditch the subpage bit and check again 02147 $name = $this->getDBkey(); 02148 list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); 02149 if ( $name !== false ) { 02150 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); 02151 if ( in_array( $pure, $wgWhitelistRead, true ) ) { 02152 $whitelisted = true; 02153 } 02154 } 02155 } 02156 } 02157 02158 if ( !$whitelisted ) { 02159 # If the title is not whitelisted, give extensions a chance to do so... 02160 wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); 02161 if ( !$whitelisted ) { 02162 $errors[] = $this->missingPermissionError( $action, $short ); 02163 } 02164 } 02165 02166 return $errors; 02167 } 02168 02177 private function missingPermissionError( $action, $short ) { 02178 // We avoid expensive display logic for quickUserCan's and such 02179 if ( $short ) { 02180 return array( 'badaccess-group0' ); 02181 } 02182 02183 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), 02184 User::getGroupsWithPermission( $action ) ); 02185 02186 if ( count( $groups ) ) { 02187 global $wgLang; 02188 return array( 02189 'badaccess-groups', 02190 $wgLang->commaList( $groups ), 02191 count( $groups ) 02192 ); 02193 } else { 02194 return array( 'badaccess-group0' ); 02195 } 02196 } 02197 02209 protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { 02210 wfProfileIn( __METHOD__ ); 02211 02212 # Read has special handling 02213 if ( $action == 'read' ) { 02214 $checks = array( 02215 'checkPermissionHooks', 02216 'checkReadPermissions', 02217 ); 02218 } else { 02219 $checks = array( 02220 'checkQuickPermissions', 02221 'checkPermissionHooks', 02222 'checkSpecialsAndNSPermissions', 02223 'checkCSSandJSPermissions', 02224 'checkPageRestrictions', 02225 'checkCascadingSourcesRestrictions', 02226 'checkActionPermissions', 02227 'checkUserBlock' 02228 ); 02229 } 02230 02231 $errors = array(); 02232 while( count( $checks ) > 0 && 02233 !( $short && count( $errors ) > 0 ) ) { 02234 $method = array_shift( $checks ); 02235 $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); 02236 } 02237 02238 wfProfileOut( __METHOD__ ); 02239 return $errors; 02240 } 02241 02249 public function userCanEditCssSubpage() { 02250 global $wgUser; 02251 wfDeprecated( __METHOD__, '1.19' ); 02252 return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) ) 02253 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); 02254 } 02255 02263 public function userCanEditJsSubpage() { 02264 global $wgUser; 02265 wfDeprecated( __METHOD__, '1.19' ); 02266 return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) ) 02267 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); 02268 } 02269 02277 public static function getFilteredRestrictionTypes( $exists = true ) { 02278 global $wgRestrictionTypes; 02279 $types = $wgRestrictionTypes; 02280 if ( $exists ) { 02281 # Remove the create restriction for existing titles 02282 $types = array_diff( $types, array( 'create' ) ); 02283 } else { 02284 # Only the create and upload restrictions apply to non-existing titles 02285 $types = array_intersect( $types, array( 'create', 'upload' ) ); 02286 } 02287 return $types; 02288 } 02289 02295 public function getRestrictionTypes() { 02296 if ( $this->isSpecialPage() ) { 02297 return array(); 02298 } 02299 02300 $types = self::getFilteredRestrictionTypes( $this->exists() ); 02301 02302 if ( $this->getNamespace() != NS_FILE ) { 02303 # Remove the upload restriction for non-file titles 02304 $types = array_diff( $types, array( 'upload' ) ); 02305 } 02306 02307 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); 02308 02309 wfDebug( __METHOD__ . ': applicable restrictions to [[' . 02310 $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); 02311 02312 return $types; 02313 } 02314 02322 private function getTitleProtection() { 02323 // Can't protect pages in special namespaces 02324 if ( $this->getNamespace() < 0 ) { 02325 return false; 02326 } 02327 02328 // Can't protect pages that exist. 02329 if ( $this->exists() ) { 02330 return false; 02331 } 02332 02333 if ( !isset( $this->mTitleProtection ) ) { 02334 $dbr = wfGetDB( DB_SLAVE ); 02335 $res = $dbr->select( 'protected_titles', '*', 02336 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02337 __METHOD__ ); 02338 02339 // fetchRow returns false if there are no rows. 02340 $this->mTitleProtection = $dbr->fetchRow( $res ); 02341 } 02342 return $this->mTitleProtection; 02343 } 02344 02354 public function updateTitleProtection( $create_perm, $reason, $expiry ) { 02355 wfDeprecated( __METHOD__, '1.19' ); 02356 02357 global $wgUser; 02358 02359 $limit = array( 'create' => $create_perm ); 02360 $expiry = array( 'create' => $expiry ); 02361 02362 $page = WikiPage::factory( $this ); 02363 $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser ); 02364 02365 return $status->isOK(); 02366 } 02367 02371 public function deleteTitleProtection() { 02372 $dbw = wfGetDB( DB_MASTER ); 02373 02374 $dbw->delete( 02375 'protected_titles', 02376 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02377 __METHOD__ 02378 ); 02379 $this->mTitleProtection = false; 02380 } 02381 02388 public function isSemiProtected( $action = 'edit' ) { 02389 if ( $this->exists() ) { 02390 $restrictions = $this->getRestrictions( $action ); 02391 if ( count( $restrictions ) > 0 ) { 02392 foreach ( $restrictions as $restriction ) { 02393 if ( strtolower( $restriction ) != 'autoconfirmed' ) { 02394 return false; 02395 } 02396 } 02397 } else { 02398 # Not protected 02399 return false; 02400 } 02401 return true; 02402 } else { 02403 # If it doesn't exist, it can't be protected 02404 return false; 02405 } 02406 } 02407 02415 public function isProtected( $action = '' ) { 02416 global $wgRestrictionLevels; 02417 02418 $restrictionTypes = $this->getRestrictionTypes(); 02419 02420 # Special pages have inherent protection 02421 if( $this->isSpecialPage() ) { 02422 return true; 02423 } 02424 02425 # Check regular protection levels 02426 foreach ( $restrictionTypes as $type ) { 02427 if ( $action == $type || $action == '' ) { 02428 $r = $this->getRestrictions( $type ); 02429 foreach ( $wgRestrictionLevels as $level ) { 02430 if ( in_array( $level, $r ) && $level != '' ) { 02431 return true; 02432 } 02433 } 02434 } 02435 } 02436 02437 return false; 02438 } 02439 02447 public function isNamespaceProtected( User $user ) { 02448 global $wgNamespaceProtection; 02449 02450 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { 02451 foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { 02452 if ( $right != '' && !$user->isAllowed( $right ) ) { 02453 return true; 02454 } 02455 } 02456 } 02457 return false; 02458 } 02459 02465 public function isCascadeProtected() { 02466 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); 02467 return ( $sources > 0 ); 02468 } 02469 02480 public function getCascadeProtectionSources( $getPages = true ) { 02481 global $wgContLang; 02482 $pagerestrictions = array(); 02483 02484 if ( isset( $this->mCascadeSources ) && $getPages ) { 02485 return array( $this->mCascadeSources, $this->mCascadingRestrictions ); 02486 } elseif ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) { 02487 return array( $this->mHasCascadingRestrictions, $pagerestrictions ); 02488 } 02489 02490 wfProfileIn( __METHOD__ ); 02491 02492 $dbr = wfGetDB( DB_SLAVE ); 02493 02494 if ( $this->getNamespace() == NS_FILE ) { 02495 $tables = array( 'imagelinks', 'page_restrictions' ); 02496 $where_clauses = array( 02497 'il_to' => $this->getDBkey(), 02498 'il_from=pr_page', 02499 'pr_cascade' => 1 02500 ); 02501 } else { 02502 $tables = array( 'templatelinks', 'page_restrictions' ); 02503 $where_clauses = array( 02504 'tl_namespace' => $this->getNamespace(), 02505 'tl_title' => $this->getDBkey(), 02506 'tl_from=pr_page', 02507 'pr_cascade' => 1 02508 ); 02509 } 02510 02511 if ( $getPages ) { 02512 $cols = array( 'pr_page', 'page_namespace', 'page_title', 02513 'pr_expiry', 'pr_type', 'pr_level' ); 02514 $where_clauses[] = 'page_id=pr_page'; 02515 $tables[] = 'page'; 02516 } else { 02517 $cols = array( 'pr_expiry' ); 02518 } 02519 02520 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); 02521 02522 $sources = $getPages ? array() : false; 02523 $now = wfTimestampNow(); 02524 $purgeExpired = false; 02525 02526 foreach ( $res as $row ) { 02527 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02528 if ( $expiry > $now ) { 02529 if ( $getPages ) { 02530 $page_id = $row->pr_page; 02531 $page_ns = $row->page_namespace; 02532 $page_title = $row->page_title; 02533 $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); 02534 # Add groups needed for each restriction type if its not already there 02535 # Make sure this restriction type still exists 02536 02537 if ( !isset( $pagerestrictions[$row->pr_type] ) ) { 02538 $pagerestrictions[$row->pr_type] = array(); 02539 } 02540 02541 if ( isset( $pagerestrictions[$row->pr_type] ) && 02542 !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) ) { 02543 $pagerestrictions[$row->pr_type][] = $row->pr_level; 02544 } 02545 } else { 02546 $sources = true; 02547 } 02548 } else { 02549 // Trigger lazy purge of expired restrictions from the db 02550 $purgeExpired = true; 02551 } 02552 } 02553 if ( $purgeExpired ) { 02554 Title::purgeExpiredRestrictions(); 02555 } 02556 02557 if ( $getPages ) { 02558 $this->mCascadeSources = $sources; 02559 $this->mCascadingRestrictions = $pagerestrictions; 02560 } else { 02561 $this->mHasCascadingRestrictions = $sources; 02562 } 02563 02564 wfProfileOut( __METHOD__ ); 02565 return array( $sources, $pagerestrictions ); 02566 } 02567 02574 public function getRestrictions( $action ) { 02575 if ( !$this->mRestrictionsLoaded ) { 02576 $this->loadRestrictions(); 02577 } 02578 return isset( $this->mRestrictions[$action] ) 02579 ? $this->mRestrictions[$action] 02580 : array(); 02581 } 02582 02590 public function getRestrictionExpiry( $action ) { 02591 if ( !$this->mRestrictionsLoaded ) { 02592 $this->loadRestrictions(); 02593 } 02594 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; 02595 } 02596 02602 function areRestrictionsCascading() { 02603 if ( !$this->mRestrictionsLoaded ) { 02604 $this->loadRestrictions(); 02605 } 02606 02607 return $this->mCascadeRestriction; 02608 } 02609 02617 private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { 02618 $rows = array(); 02619 02620 foreach ( $res as $row ) { 02621 $rows[] = $row; 02622 } 02623 02624 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); 02625 } 02626 02636 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { 02637 global $wgContLang; 02638 $dbr = wfGetDB( DB_SLAVE ); 02639 02640 $restrictionTypes = $this->getRestrictionTypes(); 02641 02642 foreach ( $restrictionTypes as $type ) { 02643 $this->mRestrictions[$type] = array(); 02644 $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); 02645 } 02646 02647 $this->mCascadeRestriction = false; 02648 02649 # Backwards-compatibility: also load the restrictions from the page record (old format). 02650 02651 if ( $oldFashionedRestrictions === null ) { 02652 $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', 02653 array( 'page_id' => $this->getArticleID() ), __METHOD__ ); 02654 } 02655 02656 if ( $oldFashionedRestrictions != '' ) { 02657 02658 foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { 02659 $temp = explode( '=', trim( $restrict ) ); 02660 if ( count( $temp ) == 1 ) { 02661 // old old format should be treated as edit/move restriction 02662 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); 02663 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); 02664 } else { 02665 $restriction = trim( $temp[1] ); 02666 if( $restriction != '' ) { //some old entries are empty 02667 $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); 02668 } 02669 } 02670 } 02671 02672 $this->mOldRestrictions = true; 02673 02674 } 02675 02676 if ( count( $rows ) ) { 02677 # Current system - load second to make them override. 02678 $now = wfTimestampNow(); 02679 $purgeExpired = false; 02680 02681 # Cycle through all the restrictions. 02682 foreach ( $rows as $row ) { 02683 02684 // Don't take care of restrictions types that aren't allowed 02685 if ( !in_array( $row->pr_type, $restrictionTypes ) ) 02686 continue; 02687 02688 // This code should be refactored, now that it's being used more generally, 02689 // But I don't really see any harm in leaving it in Block for now -werdna 02690 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02691 02692 // Only apply the restrictions if they haven't expired! 02693 if ( !$expiry || $expiry > $now ) { 02694 $this->mRestrictionsExpiry[$row->pr_type] = $expiry; 02695 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); 02696 02697 $this->mCascadeRestriction |= $row->pr_cascade; 02698 } else { 02699 // Trigger a lazy purge of expired restrictions 02700 $purgeExpired = true; 02701 } 02702 } 02703 02704 if ( $purgeExpired ) { 02705 Title::purgeExpiredRestrictions(); 02706 } 02707 } 02708 02709 $this->mRestrictionsLoaded = true; 02710 } 02711 02718 public function loadRestrictions( $oldFashionedRestrictions = null ) { 02719 global $wgContLang; 02720 if ( !$this->mRestrictionsLoaded ) { 02721 if ( $this->exists() ) { 02722 $dbr = wfGetDB( DB_SLAVE ); 02723 02724 $res = $dbr->select( 02725 'page_restrictions', 02726 '*', 02727 array( 'pr_page' => $this->getArticleID() ), 02728 __METHOD__ 02729 ); 02730 02731 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); 02732 } else { 02733 $title_protection = $this->getTitleProtection(); 02734 02735 if ( $title_protection ) { 02736 $now = wfTimestampNow(); 02737 $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); 02738 02739 if ( !$expiry || $expiry > $now ) { 02740 // Apply the restrictions 02741 $this->mRestrictionsExpiry['create'] = $expiry; 02742 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); 02743 } else { // Get rid of the old restrictions 02744 Title::purgeExpiredRestrictions(); 02745 $this->mTitleProtection = false; 02746 } 02747 } else { 02748 $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); 02749 } 02750 $this->mRestrictionsLoaded = true; 02751 } 02752 } 02753 } 02754 02759 public function flushRestrictions() { 02760 $this->mRestrictionsLoaded = false; 02761 $this->mTitleProtection = null; 02762 } 02763 02767 static function purgeExpiredRestrictions() { 02768 $dbw = wfGetDB( DB_MASTER ); 02769 $dbw->delete( 02770 'page_restrictions', 02771 array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02772 __METHOD__ 02773 ); 02774 02775 $dbw->delete( 02776 'protected_titles', 02777 array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02778 __METHOD__ 02779 ); 02780 } 02781 02787 public function hasSubpages() { 02788 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 02789 # Duh 02790 return false; 02791 } 02792 02793 # We dynamically add a member variable for the purpose of this method 02794 # alone to cache the result. There's no point in having it hanging 02795 # around uninitialized in every Title object; therefore we only add it 02796 # if needed and don't declare it statically. 02797 if ( isset( $this->mHasSubpages ) ) { 02798 return $this->mHasSubpages; 02799 } 02800 02801 $subpages = $this->getSubpages( 1 ); 02802 if ( $subpages instanceof TitleArray ) { 02803 return $this->mHasSubpages = (bool)$subpages->count(); 02804 } 02805 return $this->mHasSubpages = false; 02806 } 02807 02815 public function getSubpages( $limit = -1 ) { 02816 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 02817 return array(); 02818 } 02819 02820 $dbr = wfGetDB( DB_SLAVE ); 02821 $conds['page_namespace'] = $this->getNamespace(); 02822 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); 02823 $options = array(); 02824 if ( $limit > -1 ) { 02825 $options['LIMIT'] = $limit; 02826 } 02827 return $this->mSubpages = TitleArray::newFromResult( 02828 $dbr->select( 'page', 02829 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), 02830 $conds, 02831 __METHOD__, 02832 $options 02833 ) 02834 ); 02835 } 02836 02842 public function isDeleted() { 02843 if ( $this->getNamespace() < 0 ) { 02844 $n = 0; 02845 } else { 02846 $dbr = wfGetDB( DB_SLAVE ); 02847 02848 $n = $dbr->selectField( 'archive', 'COUNT(*)', 02849 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02850 __METHOD__ 02851 ); 02852 if ( $this->getNamespace() == NS_FILE ) { 02853 $n += $dbr->selectField( 'filearchive', 'COUNT(*)', 02854 array( 'fa_name' => $this->getDBkey() ), 02855 __METHOD__ 02856 ); 02857 } 02858 } 02859 return (int)$n; 02860 } 02861 02867 public function isDeletedQuick() { 02868 if ( $this->getNamespace() < 0 ) { 02869 return false; 02870 } 02871 $dbr = wfGetDB( DB_SLAVE ); 02872 $deleted = (bool)$dbr->selectField( 'archive', '1', 02873 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02874 __METHOD__ 02875 ); 02876 if ( !$deleted && $this->getNamespace() == NS_FILE ) { 02877 $deleted = (bool)$dbr->selectField( 'filearchive', '1', 02878 array( 'fa_name' => $this->getDBkey() ), 02879 __METHOD__ 02880 ); 02881 } 02882 return $deleted; 02883 } 02884 02893 public function getArticleID( $flags = 0 ) { 02894 if ( $this->getNamespace() < 0 ) { 02895 return $this->mArticleID = 0; 02896 } 02897 $linkCache = LinkCache::singleton(); 02898 if ( $flags & self::GAID_FOR_UPDATE ) { 02899 $oldUpdate = $linkCache->forUpdate( true ); 02900 $linkCache->clearLink( $this ); 02901 $this->mArticleID = $linkCache->addLinkObj( $this ); 02902 $linkCache->forUpdate( $oldUpdate ); 02903 } else { 02904 if ( -1 == $this->mArticleID ) { 02905 $this->mArticleID = $linkCache->addLinkObj( $this ); 02906 } 02907 } 02908 return $this->mArticleID; 02909 } 02910 02918 public function isRedirect( $flags = 0 ) { 02919 if ( !is_null( $this->mRedirect ) ) { 02920 return $this->mRedirect; 02921 } 02922 # Calling getArticleID() loads the field from cache as needed 02923 if ( !$this->getArticleID( $flags ) ) { 02924 return $this->mRedirect = false; 02925 } 02926 02927 $linkCache = LinkCache::singleton(); 02928 $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' ); 02929 if ( $cached === null ) { 02930 // TODO: check the assumption that the cache actually knows about this title 02931 // and handle this, such as get the title from the database. 02932 // See https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 02933 wfDebug( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); 02934 wfDebug( wfBacktrace() ); 02935 } 02936 02937 $this->mRedirect = (bool)$cached; 02938 02939 return $this->mRedirect; 02940 } 02941 02949 public function getLength( $flags = 0 ) { 02950 if ( $this->mLength != -1 ) { 02951 return $this->mLength; 02952 } 02953 # Calling getArticleID() loads the field from cache as needed 02954 if ( !$this->getArticleID( $flags ) ) { 02955 return $this->mLength = 0; 02956 } 02957 $linkCache = LinkCache::singleton(); 02958 $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' ); 02959 if ( $cached === null ) { # check the assumption that the cache actually knows about this title 02960 # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 02961 # as a stop gap, perhaps log this, but don't throw an exception? 02962 wfDebug( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); 02963 wfDebug( wfBacktrace() ); 02964 } 02965 02966 $this->mLength = intval( $cached ); 02967 02968 return $this->mLength; 02969 } 02970 02977 public function getLatestRevID( $flags = 0 ) { 02978 if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) { 02979 return intval( $this->mLatestID ); 02980 } 02981 # Calling getArticleID() loads the field from cache as needed 02982 if ( !$this->getArticleID( $flags ) ) { 02983 return $this->mLatestID = 0; 02984 } 02985 $linkCache = LinkCache::singleton(); 02986 $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' ); 02987 if ( $cached === null ) { # check the assumption that the cache actually knows about this title 02988 # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 02989 # as a stop gap, perhaps log this, but don't throw an exception? 02990 throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); 02991 } 02992 02993 $this->mLatestID = intval( $cached ); 02994 02995 return $this->mLatestID; 02996 } 02997 03008 public function resetArticleID( $newid ) { 03009 $linkCache = LinkCache::singleton(); 03010 $linkCache->clearLink( $this ); 03011 03012 if ( $newid === false ) { 03013 $this->mArticleID = -1; 03014 } else { 03015 $this->mArticleID = intval( $newid ); 03016 } 03017 $this->mRestrictionsLoaded = false; 03018 $this->mRestrictions = array(); 03019 $this->mRedirect = null; 03020 $this->mLength = -1; 03021 $this->mLatestID = false; 03022 $this->mContentModel = false; 03023 $this->mEstimateRevisions = null; 03024 } 03025 03033 public static function capitalize( $text, $ns = NS_MAIN ) { 03034 global $wgContLang; 03035 03036 if ( MWNamespace::isCapitalized( $ns ) ) { 03037 return $wgContLang->ucfirst( $text ); 03038 } else { 03039 return $text; 03040 } 03041 } 03042 03054 private function secureAndSplit() { 03055 global $wgContLang, $wgLocalInterwiki; 03056 03057 # Initialisation 03058 $this->mInterwiki = $this->mFragment = ''; 03059 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN 03060 03061 $dbkey = $this->mDbkeyform; 03062 03063 # Strip Unicode bidi override characters. 03064 # Sometimes they slip into cut-n-pasted page titles, where the 03065 # override chars get included in list displays. 03066 $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey ); 03067 03068 # Clean up whitespace 03069 # Note: use of the /u option on preg_replace here will cause 03070 # input with invalid UTF-8 sequences to be nullified out in PHP 5.2.x, 03071 # conveniently disabling them. 03072 $dbkey = preg_replace( '/[ _\xA0\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u', '_', $dbkey ); 03073 $dbkey = trim( $dbkey, '_' ); 03074 03075 if ( $dbkey == '' ) { 03076 return false; 03077 } 03078 03079 if ( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) { 03080 # Contained illegal UTF-8 sequences or forbidden Unicode chars. 03081 return false; 03082 } 03083 03084 $this->mDbkeyform = $dbkey; 03085 03086 # Initial colon indicates main namespace rather than specified default 03087 # but should not create invalid {ns,title} pairs such as {0,Project:Foo} 03088 if ( ':' == $dbkey[0] ) { 03089 $this->mNamespace = NS_MAIN; 03090 $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing 03091 $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace 03092 } 03093 03094 # Namespace or interwiki prefix 03095 $firstPass = true; 03096 $prefixRegexp = "/^(.+?)_*:_*(.*)$/S"; 03097 do { 03098 $m = array(); 03099 if ( preg_match( $prefixRegexp, $dbkey, $m ) ) { 03100 $p = $m[1]; 03101 if ( ( $ns = $wgContLang->getNsIndex( $p ) ) !== false ) { 03102 # Ordinary namespace 03103 $dbkey = $m[2]; 03104 $this->mNamespace = $ns; 03105 # For Talk:X pages, check if X has a "namespace" prefix 03106 if ( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) { 03107 if ( $wgContLang->getNsIndex( $x[1] ) ) { 03108 # Disallow Talk:File:x type titles... 03109 return false; 03110 } elseif ( Interwiki::isValidInterwiki( $x[1] ) ) { 03111 # Disallow Talk:Interwiki:x type titles... 03112 return false; 03113 } 03114 } 03115 } elseif ( Interwiki::isValidInterwiki( $p ) ) { 03116 if ( !$firstPass ) { 03117 # Can't make a local interwiki link to an interwiki link. 03118 # That's just crazy! 03119 return false; 03120 } 03121 03122 # Interwiki link 03123 $dbkey = $m[2]; 03124 $this->mInterwiki = $wgContLang->lc( $p ); 03125 03126 # Redundant interwiki prefix to the local wiki 03127 if ( $wgLocalInterwiki !== false 03128 && 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) 03129 { 03130 if ( $dbkey == '' ) { 03131 # Can't have an empty self-link 03132 return false; 03133 } 03134 $this->mInterwiki = ''; 03135 $firstPass = false; 03136 # Do another namespace split... 03137 continue; 03138 } 03139 03140 # If there's an initial colon after the interwiki, that also 03141 # resets the default namespace 03142 if ( $dbkey !== '' && $dbkey[0] == ':' ) { 03143 $this->mNamespace = NS_MAIN; 03144 $dbkey = substr( $dbkey, 1 ); 03145 } 03146 } 03147 # If there's no recognized interwiki or namespace, 03148 # then let the colon expression be part of the title. 03149 } 03150 break; 03151 } while ( true ); 03152 03153 # We already know that some pages won't be in the database! 03154 if ( $this->mInterwiki != '' || NS_SPECIAL == $this->mNamespace ) { 03155 $this->mArticleID = 0; 03156 } 03157 $fragment = strstr( $dbkey, '#' ); 03158 if ( false !== $fragment ) { 03159 $this->setFragment( $fragment ); 03160 $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) ); 03161 # remove whitespace again: prevents "Foo_bar_#" 03162 # becoming "Foo_bar_" 03163 $dbkey = preg_replace( '/_*$/', '', $dbkey ); 03164 } 03165 03166 # Reject illegal characters. 03167 $rxTc = self::getTitleInvalidRegex(); 03168 if ( preg_match( $rxTc, $dbkey ) ) { 03169 return false; 03170 } 03171 03172 # Pages with "/./" or "/../" appearing in the URLs will often be un- 03173 # reachable due to the way web browsers deal with 'relative' URLs. 03174 # Also, they conflict with subpage syntax. Forbid them explicitly. 03175 if ( strpos( $dbkey, '.' ) !== false && 03176 ( $dbkey === '.' || $dbkey === '..' || 03177 strpos( $dbkey, './' ) === 0 || 03178 strpos( $dbkey, '../' ) === 0 || 03179 strpos( $dbkey, '/./' ) !== false || 03180 strpos( $dbkey, '/../' ) !== false || 03181 substr( $dbkey, -2 ) == '/.' || 03182 substr( $dbkey, -3 ) == '/..' ) ) 03183 { 03184 return false; 03185 } 03186 03187 # Magic tilde sequences? Nu-uh! 03188 if ( strpos( $dbkey, '~~~' ) !== false ) { 03189 return false; 03190 } 03191 03192 # Limit the size of titles to 255 bytes. This is typically the size of the 03193 # underlying database field. We make an exception for special pages, which 03194 # don't need to be stored in the database, and may edge over 255 bytes due 03195 # to subpage syntax for long titles, e.g. [[Special:Block/Long name]] 03196 if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) || 03197 strlen( $dbkey ) > 512 ) 03198 { 03199 return false; 03200 } 03201 03202 # Normally, all wiki links are forced to have an initial capital letter so [[foo]] 03203 # and [[Foo]] point to the same place. Don't force it for interwikis, since the 03204 # other site might be case-sensitive. 03205 $this->mUserCaseDBKey = $dbkey; 03206 if ( $this->mInterwiki == '' ) { 03207 $dbkey = self::capitalize( $dbkey, $this->mNamespace ); 03208 } 03209 03210 # Can't make a link to a namespace alone... "empty" local links can only be 03211 # self-links with a fragment identifier. 03212 if ( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) { 03213 return false; 03214 } 03215 03216 // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles. 03217 // IP names are not allowed for accounts, and can only be referring to 03218 // edits from the IP. Given '::' abbreviations and caps/lowercaps, 03219 // there are numerous ways to present the same IP. Having sp:contribs scan 03220 // them all is silly and having some show the edits and others not is 03221 // inconsistent. Same for talk/userpages. Keep them normalized instead. 03222 $dbkey = ( $this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK ) 03223 ? IP::sanitizeIP( $dbkey ) 03224 : $dbkey; 03225 03226 // Any remaining initial :s are illegal. 03227 if ( $dbkey !== '' && ':' == $dbkey[0] ) { 03228 return false; 03229 } 03230 03231 # Fill fields 03232 $this->mDbkeyform = $dbkey; 03233 $this->mUrlform = wfUrlencode( $dbkey ); 03234 03235 $this->mTextform = str_replace( '_', ' ', $dbkey ); 03236 03237 return true; 03238 } 03239 03252 public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03253 if ( count( $options ) > 0 ) { 03254 $db = wfGetDB( DB_MASTER ); 03255 } else { 03256 $db = wfGetDB( DB_SLAVE ); 03257 } 03258 03259 $res = $db->select( 03260 array( 'page', $table ), 03261 self::getSelectFields(), 03262 array( 03263 "{$prefix}_from=page_id", 03264 "{$prefix}_namespace" => $this->getNamespace(), 03265 "{$prefix}_title" => $this->getDBkey() ), 03266 __METHOD__, 03267 $options 03268 ); 03269 03270 $retVal = array(); 03271 if ( $res->numRows() ) { 03272 $linkCache = LinkCache::singleton(); 03273 foreach ( $res as $row ) { 03274 $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ); 03275 if ( $titleObj ) { 03276 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03277 $retVal[] = $titleObj; 03278 } 03279 } 03280 } 03281 return $retVal; 03282 } 03283 03294 public function getTemplateLinksTo( $options = array() ) { 03295 return $this->getLinksTo( $options, 'templatelinks', 'tl' ); 03296 } 03297 03310 public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03311 global $wgContentHandlerUseDB; 03312 03313 $id = $this->getArticleID(); 03314 03315 # If the page doesn't exist; there can't be any link from this page 03316 if ( !$id ) { 03317 return array(); 03318 } 03319 03320 if ( count( $options ) > 0 ) { 03321 $db = wfGetDB( DB_MASTER ); 03322 } else { 03323 $db = wfGetDB( DB_SLAVE ); 03324 } 03325 03326 $namespaceFiled = "{$prefix}_namespace"; 03327 $titleField = "{$prefix}_title"; 03328 03329 $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ); 03330 if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model'; 03331 03332 $res = $db->select( 03333 array( $table, 'page' ), 03334 $fields, 03335 array( "{$prefix}_from" => $id ), 03336 __METHOD__, 03337 $options, 03338 array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) ) 03339 ); 03340 03341 $retVal = array(); 03342 if ( $res->numRows() ) { 03343 $linkCache = LinkCache::singleton(); 03344 foreach ( $res as $row ) { 03345 $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); 03346 if ( $titleObj ) { 03347 if ( $row->page_id ) { 03348 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03349 } else { 03350 $linkCache->addBadLinkObj( $titleObj ); 03351 } 03352 $retVal[] = $titleObj; 03353 } 03354 } 03355 } 03356 return $retVal; 03357 } 03358 03369 public function getTemplateLinksFrom( $options = array() ) { 03370 return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); 03371 } 03372 03379 public function getBrokenLinksFrom() { 03380 if ( $this->getArticleID() == 0 ) { 03381 # All links from article ID 0 are false positives 03382 return array(); 03383 } 03384 03385 $dbr = wfGetDB( DB_SLAVE ); 03386 $res = $dbr->select( 03387 array( 'page', 'pagelinks' ), 03388 array( 'pl_namespace', 'pl_title' ), 03389 array( 03390 'pl_from' => $this->getArticleID(), 03391 'page_namespace IS NULL' 03392 ), 03393 __METHOD__, array(), 03394 array( 03395 'page' => array( 03396 'LEFT JOIN', 03397 array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) 03398 ) 03399 ) 03400 ); 03401 03402 $retVal = array(); 03403 foreach ( $res as $row ) { 03404 $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); 03405 } 03406 return $retVal; 03407 } 03408 03409 03416 public function getSquidURLs() { 03417 $urls = array( 03418 $this->getInternalURL(), 03419 $this->getInternalURL( 'action=history' ) 03420 ); 03421 03422 $pageLang = $this->getPageLanguage(); 03423 if ( $pageLang->hasVariants() ) { 03424 $variants = $pageLang->getVariants(); 03425 foreach ( $variants as $vCode ) { 03426 $urls[] = $this->getInternalURL( '', $vCode ); 03427 } 03428 } 03429 03430 return $urls; 03431 } 03432 03436 public function purgeSquid() { 03437 global $wgUseSquid; 03438 if ( $wgUseSquid ) { 03439 $urls = $this->getSquidURLs(); 03440 $u = new SquidUpdate( $urls ); 03441 $u->doUpdate(); 03442 } 03443 } 03444 03451 public function moveNoAuth( &$nt ) { 03452 return $this->moveTo( $nt, false ); 03453 } 03454 03465 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { 03466 global $wgUser, $wgContentHandlerUseDB; 03467 03468 $errors = array(); 03469 if ( !$nt ) { 03470 // Normally we'd add this to $errors, but we'll get 03471 // lots of syntax errors if $nt is not an object 03472 return array( array( 'badtitletext' ) ); 03473 } 03474 if ( $this->equals( $nt ) ) { 03475 $errors[] = array( 'selfmove' ); 03476 } 03477 if ( !$this->isMovable() ) { 03478 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 03479 } 03480 if ( $nt->getInterwiki() != '' ) { 03481 $errors[] = array( 'immobile-target-namespace-iw' ); 03482 } 03483 if ( !$nt->isMovable() ) { 03484 $errors[] = array( 'immobile-target-namespace', $nt->getNsText() ); 03485 } 03486 03487 $oldid = $this->getArticleID(); 03488 $newid = $nt->getArticleID(); 03489 03490 if ( strlen( $nt->getDBkey() ) < 1 ) { 03491 $errors[] = array( 'articleexists' ); 03492 } 03493 if ( ( $this->getDBkey() == '' ) || 03494 ( !$oldid ) || 03495 ( $nt->getDBkey() == '' ) ) { 03496 $errors[] = array( 'badarticleerror' ); 03497 } 03498 03499 // Content model checks 03500 if ( !$wgContentHandlerUseDB && 03501 $this->getContentModel() !== $nt->getContentModel() ) { 03502 // can't move a page if that would change the page's content model 03503 $errors[] = array( 'bad-target-model', 03504 ContentHandler::getLocalizedName( $this->getContentModel() ), 03505 ContentHandler::getLocalizedName( $nt->getContentModel() ) ); 03506 } 03507 03508 // Image-specific checks 03509 if ( $this->getNamespace() == NS_FILE ) { 03510 $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) ); 03511 } 03512 03513 if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) { 03514 $errors[] = array( 'nonfile-cannot-move-to-file' ); 03515 } 03516 03517 if ( $auth ) { 03518 $errors = wfMergeErrorArrays( $errors, 03519 $this->getUserPermissionsErrors( 'move', $wgUser ), 03520 $this->getUserPermissionsErrors( 'edit', $wgUser ), 03521 $nt->getUserPermissionsErrors( 'move-target', $wgUser ), 03522 $nt->getUserPermissionsErrors( 'edit', $wgUser ) ); 03523 } 03524 03525 $match = EditPage::matchSummarySpamRegex( $reason ); 03526 if ( $match !== false ) { 03527 // This is kind of lame, won't display nice 03528 $errors[] = array( 'spamprotectiontext' ); 03529 } 03530 03531 $err = null; 03532 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) { 03533 $errors[] = array( 'hookaborted', $err ); 03534 } 03535 03536 # The move is allowed only if (1) the target doesn't exist, or 03537 # (2) the target is a redirect to the source, and has no history 03538 # (so we can undo bad moves right after they're done). 03539 03540 if ( 0 != $newid ) { # Target exists; check for validity 03541 if ( !$this->isValidMoveTarget( $nt ) ) { 03542 $errors[] = array( 'articleexists' ); 03543 } 03544 } else { 03545 $tp = $nt->getTitleProtection(); 03546 $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm']; 03547 if ( $tp and !$wgUser->isAllowed( $right ) ) { 03548 $errors[] = array( 'cantmove-titleprotected' ); 03549 } 03550 } 03551 if ( empty( $errors ) ) { 03552 return true; 03553 } 03554 return $errors; 03555 } 03556 03562 protected function validateFileMoveOperation( $nt ) { 03563 global $wgUser; 03564 03565 $errors = array(); 03566 03567 // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below 03568 03569 $file = wfLocalFile( $this ); 03570 if ( $file->exists() ) { 03571 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { 03572 $errors[] = array( 'imageinvalidfilename' ); 03573 } 03574 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) { 03575 $errors[] = array( 'imagetypemismatch' ); 03576 } 03577 } 03578 03579 if ( $nt->getNamespace() != NS_FILE ) { 03580 $errors[] = array( 'imagenocrossnamespace' ); 03581 // From here we want to do checks on a file object, so if we can't 03582 // create one, we must return. 03583 return $errors; 03584 } 03585 03586 // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here 03587 03588 $destFile = wfLocalFile( $nt ); 03589 if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) { 03590 $errors[] = array( 'file-exists-sharedrepo' ); 03591 } 03592 03593 return $errors; 03594 } 03595 03607 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { 03608 global $wgUser; 03609 $err = $this->isValidMoveOperation( $nt, $auth, $reason ); 03610 if ( is_array( $err ) ) { 03611 // Auto-block user's IP if the account was "hard" blocked 03612 $wgUser->spreadAnyEditBlock(); 03613 return $err; 03614 } 03615 // Check suppressredirect permission 03616 if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) { 03617 $createRedirect = true; 03618 } 03619 03620 // If it is a file, move it first. 03621 // It is done before all other moving stuff is done because it's hard to revert. 03622 $dbw = wfGetDB( DB_MASTER ); 03623 if ( $this->getNamespace() == NS_FILE ) { 03624 $file = wfLocalFile( $this ); 03625 if ( $file->exists() ) { 03626 $status = $file->move( $nt ); 03627 if ( !$status->isOk() ) { 03628 return $status->getErrorsArray(); 03629 } 03630 } 03631 // Clear RepoGroup process cache 03632 RepoGroup::singleton()->clearCache( $this ); 03633 RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache 03634 } 03635 03636 $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own. 03637 $pageid = $this->getArticleID( self::GAID_FOR_UPDATE ); 03638 $protected = $this->isProtected(); 03639 03640 // Do the actual move 03641 $this->moveToInternal( $nt, $reason, $createRedirect ); 03642 03643 // Refresh the sortkey for this row. Be careful to avoid resetting 03644 // cl_timestamp, which may disturb time-based lists on some sites. 03645 $prefixes = $dbw->select( 03646 'categorylinks', 03647 array( 'cl_sortkey_prefix', 'cl_to' ), 03648 array( 'cl_from' => $pageid ), 03649 __METHOD__ 03650 ); 03651 foreach ( $prefixes as $prefixRow ) { 03652 $prefix = $prefixRow->cl_sortkey_prefix; 03653 $catTo = $prefixRow->cl_to; 03654 $dbw->update( 'categorylinks', 03655 array( 03656 'cl_sortkey' => Collation::singleton()->getSortKey( 03657 $nt->getCategorySortkey( $prefix ) ), 03658 'cl_timestamp=cl_timestamp' ), 03659 array( 03660 'cl_from' => $pageid, 03661 'cl_to' => $catTo ), 03662 __METHOD__ 03663 ); 03664 } 03665 03666 $redirid = $this->getArticleID(); 03667 03668 if ( $protected ) { 03669 # Protect the redirect title as the title used to be... 03670 $dbw->insertSelect( 'page_restrictions', 'page_restrictions', 03671 array( 03672 'pr_page' => $redirid, 03673 'pr_type' => 'pr_type', 03674 'pr_level' => 'pr_level', 03675 'pr_cascade' => 'pr_cascade', 03676 'pr_user' => 'pr_user', 03677 'pr_expiry' => 'pr_expiry' 03678 ), 03679 array( 'pr_page' => $pageid ), 03680 __METHOD__, 03681 array( 'IGNORE' ) 03682 ); 03683 # Update the protection log 03684 $log = new LogPage( 'protect' ); 03685 $comment = wfMessage( 03686 'prot_1movedto2', 03687 $this->getPrefixedText(), 03688 $nt->getPrefixedText() 03689 )->inContentLanguage()->text(); 03690 if ( $reason ) { 03691 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03692 } 03693 // @todo FIXME: $params? 03694 $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) ); 03695 } 03696 03697 # Update watchlists 03698 $oldnamespace = $this->getNamespace() & ~1; 03699 $newnamespace = $nt->getNamespace() & ~1; 03700 $oldtitle = $this->getDBkey(); 03701 $newtitle = $nt->getDBkey(); 03702 03703 if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) { 03704 WatchedItem::duplicateEntries( $this, $nt ); 03705 } 03706 03707 $dbw->commit( __METHOD__ ); 03708 03709 wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); 03710 return true; 03711 } 03712 03723 private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) { 03724 global $wgUser, $wgContLang; 03725 03726 if ( $nt->exists() ) { 03727 $moveOverRedirect = true; 03728 $logType = 'move_redir'; 03729 } else { 03730 $moveOverRedirect = false; 03731 $logType = 'move'; 03732 } 03733 03734 if ( $createRedirect ) { 03735 $contentHandler = ContentHandler::getForTitle( $this ); 03736 $redirectContent = $contentHandler->makeRedirectContent( $nt ); 03737 03738 // NOTE: If this page's content model does not support redirects, $redirectContent will be null. 03739 } else { 03740 $redirectContent = null; 03741 } 03742 03743 $logEntry = new ManualLogEntry( 'move', $logType ); 03744 $logEntry->setPerformer( $wgUser ); 03745 $logEntry->setTarget( $this ); 03746 $logEntry->setComment( $reason ); 03747 $logEntry->setParameters( array( 03748 '4::target' => $nt->getPrefixedText(), 03749 '5::noredir' => $redirectContent ? '0': '1', 03750 ) ); 03751 03752 $formatter = LogFormatter::newFromEntry( $logEntry ); 03753 $formatter->setContext( RequestContext::newExtraneousContext( $this ) ); 03754 $comment = $formatter->getPlainActionText(); 03755 if ( $reason ) { 03756 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03757 } 03758 # Truncate for whole multibyte characters. 03759 $comment = $wgContLang->truncate( $comment, 255 ); 03760 03761 $oldid = $this->getArticleID(); 03762 03763 $dbw = wfGetDB( DB_MASTER ); 03764 03765 $newpage = WikiPage::factory( $nt ); 03766 03767 if ( $moveOverRedirect ) { 03768 $newid = $nt->getArticleID(); 03769 03770 # Delete the old redirect. We don't save it to history since 03771 # by definition if we've got here it's rather uninteresting. 03772 # We have to remove it so that the next step doesn't trigger 03773 # a conflict on the unique namespace+title index... 03774 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ ); 03775 03776 $newpage->doDeleteUpdates( $newid ); 03777 } 03778 03779 # Save a null revision in the page's history notifying of the move 03780 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); 03781 if ( !is_object( $nullRevision ) ) { 03782 throw new MWException( 'No valid null revision produced in ' . __METHOD__ ); 03783 } 03784 03785 $nullRevision->insertOn( $dbw ); 03786 03787 # Change the name of the target page: 03788 $dbw->update( 'page', 03789 /* SET */ array( 03790 'page_namespace' => $nt->getNamespace(), 03791 'page_title' => $nt->getDBkey(), 03792 ), 03793 /* WHERE */ array( 'page_id' => $oldid ), 03794 __METHOD__ 03795 ); 03796 03797 $this->resetArticleID( 0 ); 03798 $nt->resetArticleID( $oldid ); 03799 03800 $newpage->updateRevisionOn( $dbw, $nullRevision ); 03801 03802 wfRunHooks( 'NewRevisionFromEditComplete', 03803 array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) ); 03804 03805 $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) ); 03806 03807 if ( !$moveOverRedirect ) { 03808 WikiPage::onArticleCreate( $nt ); 03809 } 03810 03811 # Recreate the redirect, this time in the other direction. 03812 if ( !$redirectContent ) { 03813 WikiPage::onArticleDelete( $this ); 03814 } else { 03815 $redirectArticle = WikiPage::factory( $this ); 03816 $newid = $redirectArticle->insertOn( $dbw ); 03817 if ( $newid ) { // sanity 03818 $redirectRevision = new Revision( array( 03819 'title' => $this, // for determining the default content model 03820 'page' => $newid, 03821 'comment' => $comment, 03822 'content' => $redirectContent ) ); 03823 $redirectRevision->insertOn( $dbw ); 03824 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); 03825 03826 wfRunHooks( 'NewRevisionFromEditComplete', 03827 array( $redirectArticle, $redirectRevision, false, $wgUser ) ); 03828 03829 $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) ); 03830 } 03831 } 03832 03833 # Log the move 03834 $logid = $logEntry->insert(); 03835 $logEntry->publish( $logid ); 03836 } 03837 03850 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { 03851 global $wgMaximumMovedPages; 03852 // Check permissions 03853 if ( !$this->userCan( 'move-subpages' ) ) { 03854 return array( 'cant-move-subpages' ); 03855 } 03856 // Do the source and target namespaces support subpages? 03857 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03858 return array( 'namespace-nosubpages', 03859 MWNamespace::getCanonicalName( $this->getNamespace() ) ); 03860 } 03861 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) { 03862 return array( 'namespace-nosubpages', 03863 MWNamespace::getCanonicalName( $nt->getNamespace() ) ); 03864 } 03865 03866 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 ); 03867 $retval = array(); 03868 $count = 0; 03869 foreach ( $subpages as $oldSubpage ) { 03870 $count++; 03871 if ( $count > $wgMaximumMovedPages ) { 03872 $retval[$oldSubpage->getPrefixedTitle()] = 03873 array( 'movepage-max-pages', 03874 $wgMaximumMovedPages ); 03875 break; 03876 } 03877 03878 // We don't know whether this function was called before 03879 // or after moving the root page, so check both 03880 // $this and $nt 03881 if ( $oldSubpage->getArticleID() == $this->getArticleID() || 03882 $oldSubpage->getArticleID() == $nt->getArticleID() ) 03883 { 03884 // When moving a page to a subpage of itself, 03885 // don't move it twice 03886 continue; 03887 } 03888 $newPageName = preg_replace( 03889 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#', 03890 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 03891 $oldSubpage->getDBkey() ); 03892 if ( $oldSubpage->isTalkPage() ) { 03893 $newNs = $nt->getTalkPage()->getNamespace(); 03894 } else { 03895 $newNs = $nt->getSubjectPage()->getNamespace(); 03896 } 03897 # Bug 14385: we need makeTitleSafe because the new page names may 03898 # be longer than 255 characters. 03899 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); 03900 03901 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); 03902 if ( $success === true ) { 03903 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); 03904 } else { 03905 $retval[$oldSubpage->getPrefixedText()] = $success; 03906 } 03907 } 03908 return $retval; 03909 } 03910 03917 public function isSingleRevRedirect() { 03918 global $wgContentHandlerUseDB; 03919 03920 $dbw = wfGetDB( DB_MASTER ); 03921 03922 # Is it a redirect? 03923 $fields = array( 'page_is_redirect', 'page_latest', 'page_id' ); 03924 if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model'; 03925 03926 $row = $dbw->selectRow( 'page', 03927 $fields, 03928 $this->pageCond(), 03929 __METHOD__, 03930 array( 'FOR UPDATE' ) 03931 ); 03932 # Cache some fields we may want 03933 $this->mArticleID = $row ? intval( $row->page_id ) : 0; 03934 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; 03935 $this->mLatestID = $row ? intval( $row->page_latest ) : false; 03936 $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false; 03937 if ( !$this->mRedirect ) { 03938 return false; 03939 } 03940 # Does the article have a history? 03941 $row = $dbw->selectField( array( 'page', 'revision' ), 03942 'rev_id', 03943 array( 'page_namespace' => $this->getNamespace(), 03944 'page_title' => $this->getDBkey(), 03945 'page_id=rev_page', 03946 'page_latest != rev_id' 03947 ), 03948 __METHOD__, 03949 array( 'FOR UPDATE' ) 03950 ); 03951 # Return true if there was no history 03952 return ( $row === false ); 03953 } 03954 03962 public function isValidMoveTarget( $nt ) { 03963 # Is it an existing file? 03964 if ( $nt->getNamespace() == NS_FILE ) { 03965 $file = wfLocalFile( $nt ); 03966 if ( $file->exists() ) { 03967 wfDebug( __METHOD__ . ": file exists\n" ); 03968 return false; 03969 } 03970 } 03971 # Is it a redirect with no history? 03972 if ( !$nt->isSingleRevRedirect() ) { 03973 wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); 03974 return false; 03975 } 03976 # Get the article text 03977 $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST ); 03978 if( !is_object( $rev ) ){ 03979 return false; 03980 } 03981 $content = $rev->getContent(); 03982 # Does the redirect point to the source? 03983 # Or is it a broken self-redirect, usually caused by namespace collisions? 03984 $redirTitle = $content ? $content->getRedirectTarget() : null; 03985 03986 if ( $redirTitle ) { 03987 if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && 03988 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) { 03989 wfDebug( __METHOD__ . ": redirect points to other page\n" ); 03990 return false; 03991 } else { 03992 return true; 03993 } 03994 } else { 03995 # Fail safe (not a redirect after all. strange.) 03996 wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . 03997 " is a redirect, but it doesn't contain a valid redirect.\n" ); 03998 return false; 03999 } 04000 } 04001 04009 public function getParentCategories() { 04010 global $wgContLang; 04011 04012 $data = array(); 04013 04014 $titleKey = $this->getArticleID(); 04015 04016 if ( $titleKey === 0 ) { 04017 return $data; 04018 } 04019 04020 $dbr = wfGetDB( DB_SLAVE ); 04021 04022 $res = $dbr->select( 'categorylinks', '*', 04023 array( 04024 'cl_from' => $titleKey, 04025 ), 04026 __METHOD__, 04027 array() 04028 ); 04029 04030 if ( $dbr->numRows( $res ) > 0 ) { 04031 foreach ( $res as $row ) { 04032 // $data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to); 04033 $data[$wgContLang->getNSText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText(); 04034 } 04035 } 04036 return $data; 04037 } 04038 04045 public function getParentCategoryTree( $children = array() ) { 04046 $stack = array(); 04047 $parents = $this->getParentCategories(); 04048 04049 if ( $parents ) { 04050 foreach ( $parents as $parent => $current ) { 04051 if ( array_key_exists( $parent, $children ) ) { 04052 # Circular reference 04053 $stack[$parent] = array(); 04054 } else { 04055 $nt = Title::newFromText( $parent ); 04056 if ( $nt ) { 04057 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) ); 04058 } 04059 } 04060 } 04061 } 04062 04063 return $stack; 04064 } 04065 04072 public function pageCond() { 04073 if ( $this->mArticleID > 0 ) { 04074 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs 04075 return array( 'page_id' => $this->mArticleID ); 04076 } else { 04077 return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); 04078 } 04079 } 04080 04088 public function getPreviousRevisionID( $revId, $flags = 0 ) { 04089 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04090 $revId = $db->selectField( 'revision', 'rev_id', 04091 array( 04092 'rev_page' => $this->getArticleID( $flags ), 04093 'rev_id < ' . intval( $revId ) 04094 ), 04095 __METHOD__, 04096 array( 'ORDER BY' => 'rev_id DESC' ) 04097 ); 04098 04099 if ( $revId === false ) { 04100 return false; 04101 } else { 04102 return intval( $revId ); 04103 } 04104 } 04105 04113 public function getNextRevisionID( $revId, $flags = 0 ) { 04114 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04115 $revId = $db->selectField( 'revision', 'rev_id', 04116 array( 04117 'rev_page' => $this->getArticleID( $flags ), 04118 'rev_id > ' . intval( $revId ) 04119 ), 04120 __METHOD__, 04121 array( 'ORDER BY' => 'rev_id' ) 04122 ); 04123 04124 if ( $revId === false ) { 04125 return false; 04126 } else { 04127 return intval( $revId ); 04128 } 04129 } 04130 04137 public function getFirstRevision( $flags = 0 ) { 04138 $pageId = $this->getArticleID( $flags ); 04139 if ( $pageId ) { 04140 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04141 $row = $db->selectRow( 'revision', Revision::selectFields(), 04142 array( 'rev_page' => $pageId ), 04143 __METHOD__, 04144 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) 04145 ); 04146 if ( $row ) { 04147 return new Revision( $row ); 04148 } 04149 } 04150 return null; 04151 } 04152 04159 public function getEarliestRevTime( $flags = 0 ) { 04160 $rev = $this->getFirstRevision( $flags ); 04161 return $rev ? $rev->getTimestamp() : null; 04162 } 04163 04169 public function isNewPage() { 04170 $dbr = wfGetDB( DB_SLAVE ); 04171 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); 04172 } 04173 04179 public function isBigDeletion() { 04180 global $wgDeleteRevisionsLimit; 04181 04182 if ( !$wgDeleteRevisionsLimit ) { 04183 return false; 04184 } 04185 04186 $revCount = $this->estimateRevisionCount(); 04187 return $revCount > $wgDeleteRevisionsLimit; 04188 } 04189 04195 public function estimateRevisionCount() { 04196 if ( !$this->exists() ) { 04197 return 0; 04198 } 04199 04200 if ( $this->mEstimateRevisions === null ) { 04201 $dbr = wfGetDB( DB_SLAVE ); 04202 $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', 04203 array( 'rev_page' => $this->getArticleID() ), __METHOD__ ); 04204 } 04205 04206 return $this->mEstimateRevisions; 04207 } 04208 04217 public function countRevisionsBetween( $old, $new ) { 04218 if ( !( $old instanceof Revision ) ) { 04219 $old = Revision::newFromTitle( $this, (int)$old ); 04220 } 04221 if ( !( $new instanceof Revision ) ) { 04222 $new = Revision::newFromTitle( $this, (int)$new ); 04223 } 04224 if ( !$old || !$new ) { 04225 return 0; // nothing to compare 04226 } 04227 $dbr = wfGetDB( DB_SLAVE ); 04228 return (int)$dbr->selectField( 'revision', 'count(*)', 04229 array( 04230 'rev_page' => $this->getArticleID(), 04231 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04232 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04233 ), 04234 __METHOD__ 04235 ); 04236 } 04237 04252 public function countAuthorsBetween( $old, $new, $limit, $options = array() ) { 04253 if ( !( $old instanceof Revision ) ) { 04254 $old = Revision::newFromTitle( $this, (int)$old ); 04255 } 04256 if ( !( $new instanceof Revision ) ) { 04257 $new = Revision::newFromTitle( $this, (int)$new ); 04258 } 04259 // XXX: what if Revision objects are passed in, but they don't refer to this title? 04260 // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID() 04261 // in the sanity check below? 04262 if ( !$old || !$new ) { 04263 return 0; // nothing to compare 04264 } 04265 $old_cmp = '>'; 04266 $new_cmp = '<'; 04267 $options = (array) $options; 04268 if ( in_array( 'include_old', $options ) ) { 04269 $old_cmp = '>='; 04270 } 04271 if ( in_array( 'include_new', $options ) ) { 04272 $new_cmp = '<='; 04273 } 04274 if ( in_array( 'include_both', $options ) ) { 04275 $old_cmp = '>='; 04276 $new_cmp = '<='; 04277 } 04278 // No DB query needed if $old and $new are the same or successive revisions: 04279 if ( $old->getId() === $new->getId() ) { 04280 return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; 04281 } else if ( $old->getId() === $new->getParentId() ) { 04282 if ( $old_cmp === '>' || $new_cmp === '<' ) { 04283 return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; 04284 } 04285 return ( $old->getRawUserText() === $new->getRawUserText() ) ? 1 : 2; 04286 } 04287 $dbr = wfGetDB( DB_SLAVE ); 04288 $res = $dbr->select( 'revision', 'DISTINCT rev_user_text', 04289 array( 04290 'rev_page' => $this->getArticleID(), 04291 "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04292 "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04293 ), __METHOD__, 04294 array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated 04295 ); 04296 return (int)$dbr->numRows( $res ); 04297 } 04298 04305 public function equals( Title $title ) { 04306 // Note: === is necessary for proper matching of number-like titles. 04307 return $this->getInterwiki() === $title->getInterwiki() 04308 && $this->getNamespace() == $title->getNamespace() 04309 && $this->getDBkey() === $title->getDBkey(); 04310 } 04311 04318 public function isSubpageOf( Title $title ) { 04319 return $this->getInterwiki() === $title->getInterwiki() 04320 && $this->getNamespace() == $title->getNamespace() 04321 && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; 04322 } 04323 04333 public function exists() { 04334 return $this->getArticleID() != 0; 04335 } 04336 04353 public function isAlwaysKnown() { 04354 $isKnown = null; 04355 04366 wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) ); 04367 04368 if ( !is_null( $isKnown ) ) { 04369 return $isKnown; 04370 } 04371 04372 if ( $this->mInterwiki != '' ) { 04373 return true; // any interwiki link might be viewable, for all we know 04374 } 04375 04376 switch( $this->mNamespace ) { 04377 case NS_MEDIA: 04378 case NS_FILE: 04379 // file exists, possibly in a foreign repo 04380 return (bool)wfFindFile( $this ); 04381 case NS_SPECIAL: 04382 // valid special page 04383 return SpecialPageFactory::exists( $this->getDBkey() ); 04384 case NS_MAIN: 04385 // selflink, possibly with fragment 04386 return $this->mDbkeyform == ''; 04387 case NS_MEDIAWIKI: 04388 // known system message 04389 return $this->hasSourceText() !== false; 04390 default: 04391 return false; 04392 } 04393 } 04394 04406 public function isKnown() { 04407 return $this->isAlwaysKnown() || $this->exists(); 04408 } 04409 04415 public function hasSourceText() { 04416 if ( $this->exists() ) { 04417 return true; 04418 } 04419 04420 if ( $this->mNamespace == NS_MEDIAWIKI ) { 04421 // If the page doesn't exist but is a known system message, default 04422 // message content will be displayed, same for language subpages- 04423 // Use always content language to avoid loading hundreds of languages 04424 // to get the link color. 04425 global $wgContLang; 04426 list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04427 $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false ); 04428 return $message->exists(); 04429 } 04430 04431 return false; 04432 } 04433 04439 public function getDefaultMessageText() { 04440 global $wgContLang; 04441 04442 if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case 04443 return false; 04444 } 04445 04446 list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04447 $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false ); 04448 04449 if ( $message->exists() ) { 04450 return $message->plain(); 04451 } else { 04452 return false; 04453 } 04454 } 04455 04461 public function invalidateCache() { 04462 if ( wfReadOnly() ) { 04463 return false; 04464 } 04465 $dbw = wfGetDB( DB_MASTER ); 04466 $success = $dbw->update( 04467 'page', 04468 array( 'page_touched' => $dbw->timestamp() ), 04469 $this->pageCond(), 04470 __METHOD__ 04471 ); 04472 HTMLFileCache::clearFileCache( $this ); 04473 return $success; 04474 } 04475 04481 public function touchLinks() { 04482 $u = new HTMLCacheUpdate( $this, 'pagelinks' ); 04483 $u->doUpdate(); 04484 04485 if ( $this->getNamespace() == NS_CATEGORY ) { 04486 $u = new HTMLCacheUpdate( $this, 'categorylinks' ); 04487 $u->doUpdate(); 04488 } 04489 } 04490 04497 public function getTouched( $db = null ) { 04498 $db = isset( $db ) ? $db : wfGetDB( DB_SLAVE ); 04499 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); 04500 return $touched; 04501 } 04502 04509 public function getNotificationTimestamp( $user = null ) { 04510 global $wgUser, $wgShowUpdatedMarker; 04511 // Assume current user if none given 04512 if ( !$user ) { 04513 $user = $wgUser; 04514 } 04515 // Check cache first 04516 $uid = $user->getId(); 04517 // avoid isset here, as it'll return false for null entries 04518 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) { 04519 return $this->mNotificationTimestamp[$uid]; 04520 } 04521 if ( !$uid || !$wgShowUpdatedMarker ) { 04522 return $this->mNotificationTimestamp[$uid] = false; 04523 } 04524 // Don't cache too much! 04525 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) { 04526 $this->mNotificationTimestamp = array(); 04527 } 04528 $dbr = wfGetDB( DB_SLAVE ); 04529 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', 04530 'wl_notificationtimestamp', 04531 array( 04532 'wl_user' => $user->getId(), 04533 'wl_namespace' => $this->getNamespace(), 04534 'wl_title' => $this->getDBkey(), 04535 ), 04536 __METHOD__ 04537 ); 04538 return $this->mNotificationTimestamp[$uid]; 04539 } 04540 04547 public function getNamespaceKey( $prepend = 'nstab-' ) { 04548 global $wgContLang; 04549 // Gets the subject namespace if this title 04550 $namespace = MWNamespace::getSubject( $this->getNamespace() ); 04551 // Checks if cononical namespace name exists for namespace 04552 if ( MWNamespace::exists( $this->getNamespace() ) ) { 04553 // Uses canonical namespace name 04554 $namespaceKey = MWNamespace::getCanonicalName( $namespace ); 04555 } else { 04556 // Uses text of namespace 04557 $namespaceKey = $this->getSubjectNsText(); 04558 } 04559 // Makes namespace key lowercase 04560 $namespaceKey = $wgContLang->lc( $namespaceKey ); 04561 // Uses main 04562 if ( $namespaceKey == '' ) { 04563 $namespaceKey = 'main'; 04564 } 04565 // Changes file to image for backwards compatibility 04566 if ( $namespaceKey == 'file' ) { 04567 $namespaceKey = 'image'; 04568 } 04569 return $prepend . $namespaceKey; 04570 } 04571 04578 public function getRedirectsHere( $ns = null ) { 04579 $redirs = array(); 04580 04581 $dbr = wfGetDB( DB_SLAVE ); 04582 $where = array( 04583 'rd_namespace' => $this->getNamespace(), 04584 'rd_title' => $this->getDBkey(), 04585 'rd_from = page_id' 04586 ); 04587 if ( $this->isExternal() ) { 04588 $where['rd_interwiki'] = $this->getInterwiki(); 04589 } else { 04590 $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'; 04591 } 04592 if ( !is_null( $ns ) ) { 04593 $where['page_namespace'] = $ns; 04594 } 04595 04596 $res = $dbr->select( 04597 array( 'redirect', 'page' ), 04598 array( 'page_namespace', 'page_title' ), 04599 $where, 04600 __METHOD__ 04601 ); 04602 04603 foreach ( $res as $row ) { 04604 $redirs[] = self::newFromRow( $row ); 04605 } 04606 return $redirs; 04607 } 04608 04614 public function isValidRedirectTarget() { 04615 global $wgInvalidRedirectTargets; 04616 04617 // invalid redirect targets are stored in a global array, but explicity disallow Userlogout here 04618 if ( $this->isSpecial( 'Userlogout' ) ) { 04619 return false; 04620 } 04621 04622 foreach ( $wgInvalidRedirectTargets as $target ) { 04623 if ( $this->isSpecial( $target ) ) { 04624 return false; 04625 } 04626 } 04627 04628 return true; 04629 } 04630 04636 public function getBacklinkCache() { 04637 return BacklinkCache::get( $this ); 04638 } 04639 04645 public function canUseNoindex() { 04646 global $wgContentNamespaces, $wgExemptFromUserRobotsControl; 04647 04648 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl ) 04649 ? $wgContentNamespaces 04650 : $wgExemptFromUserRobotsControl; 04651 04652 return !in_array( $this->mNamespace, $bannedNamespaces ); 04653 04654 } 04655 04666 public function getCategorySortkey( $prefix = '' ) { 04667 $unprefixed = $this->getText(); 04668 04669 // Anything that uses this hook should only depend 04670 // on the Title object passed in, and should probably 04671 // tell the users to run updateCollations.php --force 04672 // in order to re-sort existing category relations. 04673 wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) ); 04674 if ( $prefix !== '' ) { 04675 # Separate with a line feed, so the unprefixed part is only used as 04676 # a tiebreaker when two pages have the exact same prefix. 04677 # In UCA, tab is the only character that can sort above LF 04678 # so we strip both of them from the original prefix. 04679 $prefix = strtr( $prefix, "\n\t", ' ' ); 04680 return "$prefix\n$unprefixed"; 04681 } 04682 return $unprefixed; 04683 } 04684 04693 public function getPageLanguage() { 04694 global $wgLang; 04695 if ( $this->isSpecialPage() ) { 04696 // special pages are in the user language 04697 return $wgLang; 04698 } 04699 04700 //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request. 04701 //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language! 04702 $contentHandler = ContentHandler::getForTitle( $this ); 04703 $pageLang = $contentHandler->getPageLanguage( $this ); 04704 04705 return wfGetLangObj( $pageLang ); 04706 } 04707 04716 public function getPageViewLanguage() { 04717 global $wgLang; 04718 04719 if ( $this->isSpecialPage() ) { 04720 // If the user chooses a variant, the content is actually 04721 // in a language whose code is the variant code. 04722 $variant = $wgLang->getPreferredVariant(); 04723 if ( $wgLang->getCode() !== $variant ) { 04724 return Language::factory( $variant ); 04725 } 04726 04727 return $wgLang; 04728 } 04729 04730 //NOTE: can't be cached persistently, depends on user settings 04731 //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language! 04732 $contentHandler = ContentHandler::getForTitle( $this ); 04733 $pageLang = $contentHandler->getPageViewLanguage( $this ); 04734 return $pageLang; 04735 } 04736 }