MediaWiki
REL1_21
|
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 00094 /*protected*/ function __construct() { } 00095 00104 public static function newFromDBkey( $key ) { 00105 $t = new Title(); 00106 $t->mDbkeyform = $key; 00107 if ( $t->secureAndSplit() ) { 00108 return $t; 00109 } else { 00110 return null; 00111 } 00112 } 00113 00127 public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { 00128 if ( is_object( $text ) ) { 00129 throw new MWException( 'Title::newFromText given an object' ); 00130 } 00131 00140 if ( $defaultNamespace == NS_MAIN && isset( Title::$titleCache[$text] ) ) { 00141 return Title::$titleCache[$text]; 00142 } 00143 00144 # Convert things like é ā or 〗 into normalized (bug 14952) text 00145 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); 00146 00147 $t = new Title(); 00148 $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); 00149 $t->mDefaultNamespace = $defaultNamespace; 00150 00151 static $cachedcount = 0; 00152 if ( $t->secureAndSplit() ) { 00153 if ( $defaultNamespace == NS_MAIN ) { 00154 if ( $cachedcount >= self::CACHE_MAX ) { 00155 # Avoid memory leaks on mass operations... 00156 Title::$titleCache = array(); 00157 $cachedcount = 0; 00158 } 00159 $cachedcount++; 00160 Title::$titleCache[$text] =& $t; 00161 } 00162 return $t; 00163 } else { 00164 $ret = null; 00165 return $ret; 00166 } 00167 } 00168 00184 public static function newFromURL( $url ) { 00185 $t = new Title(); 00186 00187 # For compatibility with old buggy URLs. "+" is usually not valid in titles, 00188 # but some URLs used it as a space replacement and they still come 00189 # from some external search tools. 00190 if ( strpos( self::legalChars(), '+' ) === false ) { 00191 $url = str_replace( '+', ' ', $url ); 00192 } 00193 00194 $t->mDbkeyform = str_replace( ' ', '_', $url ); 00195 if ( $t->secureAndSplit() ) { 00196 return $t; 00197 } else { 00198 return null; 00199 } 00200 } 00201 00208 protected static function getSelectFields() { 00209 global $wgContentHandlerUseDB; 00210 00211 $fields = array( 00212 'page_namespace', 'page_title', 'page_id', 00213 'page_len', 'page_is_redirect', 'page_latest', 00214 ); 00215 00216 if ( $wgContentHandlerUseDB ) { 00217 $fields[] = 'page_content_model'; 00218 } 00219 00220 return $fields; 00221 } 00222 00230 public static function newFromID( $id, $flags = 0 ) { 00231 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 00232 $row = $db->selectRow( 00233 'page', 00234 self::getSelectFields(), 00235 array( 'page_id' => $id ), 00236 __METHOD__ 00237 ); 00238 if ( $row !== false ) { 00239 $title = Title::newFromRow( $row ); 00240 } else { 00241 $title = null; 00242 } 00243 return $title; 00244 } 00245 00252 public static function newFromIDs( $ids ) { 00253 if ( !count( $ids ) ) { 00254 return array(); 00255 } 00256 $dbr = wfGetDB( DB_SLAVE ); 00257 00258 $res = $dbr->select( 00259 'page', 00260 self::getSelectFields(), 00261 array( 'page_id' => $ids ), 00262 __METHOD__ 00263 ); 00264 00265 $titles = array(); 00266 foreach ( $res as $row ) { 00267 $titles[] = Title::newFromRow( $row ); 00268 } 00269 return $titles; 00270 } 00271 00278 public static function newFromRow( $row ) { 00279 $t = self::makeTitle( $row->page_namespace, $row->page_title ); 00280 $t->loadFromRow( $row ); 00281 return $t; 00282 } 00283 00290 public function loadFromRow( $row ) { 00291 if ( $row ) { // page found 00292 if ( isset( $row->page_id ) ) 00293 $this->mArticleID = (int)$row->page_id; 00294 if ( isset( $row->page_len ) ) 00295 $this->mLength = (int)$row->page_len; 00296 if ( isset( $row->page_is_redirect ) ) 00297 $this->mRedirect = (bool)$row->page_is_redirect; 00298 if ( isset( $row->page_latest ) ) 00299 $this->mLatestID = (int)$row->page_latest; 00300 if ( isset( $row->page_content_model ) ) 00301 $this->mContentModel = strval( $row->page_content_model ); 00302 else 00303 $this->mContentModel = false; # initialized lazily in getContentModel() 00304 } else { // page not found 00305 $this->mArticleID = 0; 00306 $this->mLength = 0; 00307 $this->mRedirect = false; 00308 $this->mLatestID = 0; 00309 $this->mContentModel = false; # initialized lazily in getContentModel() 00310 } 00311 } 00312 00326 public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { 00327 $t = new Title(); 00328 $t->mInterwiki = $interwiki; 00329 $t->mFragment = $fragment; 00330 $t->mNamespace = $ns = intval( $ns ); 00331 $t->mDbkeyform = str_replace( ' ', '_', $title ); 00332 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; 00333 $t->mUrlform = wfUrlencode( $t->mDbkeyform ); 00334 $t->mTextform = str_replace( '_', ' ', $title ); 00335 $t->mContentModel = false; # initialized lazily in getContentModel() 00336 return $t; 00337 } 00338 00350 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { 00351 if ( !MWNamespace::exists( $ns ) ) { 00352 return null; 00353 } 00354 00355 $t = new Title(); 00356 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki ); 00357 if ( $t->secureAndSplit() ) { 00358 return $t; 00359 } else { 00360 return null; 00361 } 00362 } 00363 00369 public static function newMainPage() { 00370 $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() ); 00371 // Don't give fatal errors if the message is broken 00372 if ( !$title ) { 00373 $title = Title::newFromText( 'Main Page' ); 00374 } 00375 return $title; 00376 } 00377 00388 public static function newFromRedirect( $text ) { 00389 ContentHandler::deprecated( __METHOD__, '1.21' ); 00390 00391 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00392 return $content->getRedirectTarget(); 00393 } 00394 00405 public static function newFromRedirectRecurse( $text ) { 00406 ContentHandler::deprecated( __METHOD__, '1.21' ); 00407 00408 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00409 return $content->getUltimateRedirectTarget(); 00410 } 00411 00422 public static function newFromRedirectArray( $text ) { 00423 ContentHandler::deprecated( __METHOD__, '1.21' ); 00424 00425 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00426 return $content->getRedirectChain(); 00427 } 00428 00435 public static function nameOf( $id ) { 00436 $dbr = wfGetDB( DB_SLAVE ); 00437 00438 $s = $dbr->selectRow( 00439 'page', 00440 array( 'page_namespace', 'page_title' ), 00441 array( 'page_id' => $id ), 00442 __METHOD__ 00443 ); 00444 if ( $s === false ) { 00445 return null; 00446 } 00447 00448 $n = self::makeName( $s->page_namespace, $s->page_title ); 00449 return $n; 00450 } 00451 00457 public static function legalChars() { 00458 global $wgLegalTitleChars; 00459 return $wgLegalTitleChars; 00460 } 00461 00469 static function getTitleInvalidRegex() { 00470 static $rxTc = false; 00471 if ( !$rxTc ) { 00472 # Matching titles will be held as illegal. 00473 $rxTc = '/' . 00474 # Any character not allowed is forbidden... 00475 '[^' . self::legalChars() . ']' . 00476 # URL percent encoding sequences interfere with the ability 00477 # to round-trip titles -- you can't link to them consistently. 00478 '|%[0-9A-Fa-f]{2}' . 00479 # XML/HTML character references produce similar issues. 00480 '|&[A-Za-z0-9\x80-\xff]+;' . 00481 '|&#[0-9]+;' . 00482 '|&#x[0-9A-Fa-f]+;' . 00483 '/S'; 00484 } 00485 00486 return $rxTc; 00487 } 00488 00497 public static function indexTitle( $ns, $title ) { 00498 global $wgContLang; 00499 00500 $lc = SearchEngine::legalSearchChars() . '&#;'; 00501 $t = $wgContLang->normalizeForSearch( $title ); 00502 $t = preg_replace( "/[^{$lc}]+/", ' ', $t ); 00503 $t = $wgContLang->lc( $t ); 00504 00505 # Handle 's, s' 00506 $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t ); 00507 $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t ); 00508 00509 $t = preg_replace( "/\\s+/", ' ', $t ); 00510 00511 if ( $ns == NS_FILE ) { 00512 $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t ); 00513 } 00514 return trim( $t ); 00515 } 00516 00526 public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) { 00527 global $wgContLang; 00528 00529 $namespace = $wgContLang->getNsText( $ns ); 00530 $name = $namespace == '' ? $title : "$namespace:$title"; 00531 if ( strval( $interwiki ) != '' ) { 00532 $name = "$interwiki:$name"; 00533 } 00534 if ( strval( $fragment ) != '' ) { 00535 $name .= '#' . $fragment; 00536 } 00537 return $name; 00538 } 00539 00546 static function escapeFragmentForURL( $fragment ) { 00547 # Note that we don't urlencode the fragment. urlencoded Unicode 00548 # fragments appear not to work in IE (at least up to 7) or in at least 00549 # one version of Opera 9.x. The W3C validator, for one, doesn't seem 00550 # to care if they aren't encoded. 00551 return Sanitizer::escapeId( $fragment, 'noninitial' ); 00552 } 00553 00562 public static function compare( $a, $b ) { 00563 if ( $a->getNamespace() == $b->getNamespace() ) { 00564 return strcmp( $a->getText(), $b->getText() ); 00565 } else { 00566 return $a->getNamespace() - $b->getNamespace(); 00567 } 00568 } 00569 00576 public function isLocal() { 00577 if ( $this->mInterwiki != '' ) { 00578 $iw = Interwiki::fetch( $this->mInterwiki ); 00579 if ( $iw ) { 00580 return $iw->isLocal(); 00581 } 00582 } 00583 return true; 00584 } 00585 00591 public function isExternal() { 00592 return ( $this->mInterwiki != '' ); 00593 } 00594 00600 public function getInterwiki() { 00601 return $this->mInterwiki; 00602 } 00603 00610 public function isTrans() { 00611 if ( $this->mInterwiki == '' ) { 00612 return false; 00613 } 00614 00615 return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); 00616 } 00617 00623 public function getTransWikiID() { 00624 if ( $this->mInterwiki == '' ) { 00625 return false; 00626 } 00627 00628 return Interwiki::fetch( $this->mInterwiki )->getWikiID(); 00629 } 00630 00636 public function getText() { 00637 return $this->mTextform; 00638 } 00639 00645 public function getPartialURL() { 00646 return $this->mUrlform; 00647 } 00648 00654 public function getDBkey() { 00655 return $this->mDbkeyform; 00656 } 00657 00663 function getUserCaseDBKey() { 00664 return $this->mUserCaseDBKey; 00665 } 00666 00672 public function getNamespace() { 00673 return $this->mNamespace; 00674 } 00675 00682 public function getContentModel() { 00683 if ( !$this->mContentModel ) { 00684 $linkCache = LinkCache::singleton(); 00685 $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); 00686 } 00687 00688 if ( !$this->mContentModel ) { 00689 $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); 00690 } 00691 00692 if( !$this->mContentModel ) { 00693 throw new MWException( 'Failed to determine content model!' ); 00694 } 00695 00696 return $this->mContentModel; 00697 } 00698 00705 public function hasContentModel( $id ) { 00706 return $this->getContentModel() == $id; 00707 } 00708 00714 public function getNsText() { 00715 global $wgContLang; 00716 00717 if ( $this->mInterwiki != '' ) { 00718 // This probably shouldn't even happen. ohh man, oh yuck. 00719 // But for interwiki transclusion it sometimes does. 00720 // Shit. Shit shit shit. 00721 // 00722 // Use the canonical namespaces if possible to try to 00723 // resolve a foreign namespace. 00724 if ( MWNamespace::exists( $this->mNamespace ) ) { 00725 return MWNamespace::getCanonicalName( $this->mNamespace ); 00726 } 00727 } 00728 00729 if ( $wgContLang->needsGenderDistinction() && 00730 MWNamespace::hasGenderDistinction( $this->mNamespace ) ) { 00731 $gender = GenderCache::singleton()->getGenderOf( $this->getText(), __METHOD__ ); 00732 return $wgContLang->getGenderNsText( $this->mNamespace, $gender ); 00733 } 00734 00735 return $wgContLang->getNsText( $this->mNamespace ); 00736 } 00737 00743 public function getSubjectNsText() { 00744 global $wgContLang; 00745 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) ); 00746 } 00747 00753 public function getTalkNsText() { 00754 global $wgContLang; 00755 return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) ); 00756 } 00757 00763 public function canTalk() { 00764 return( MWNamespace::canTalk( $this->mNamespace ) ); 00765 } 00766 00773 public function canExist() { 00774 return $this->mNamespace >= NS_MAIN; 00775 } 00776 00782 public function isWatchable() { 00783 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); 00784 } 00785 00791 public function isSpecialPage() { 00792 return $this->getNamespace() == NS_SPECIAL; 00793 } 00794 00801 public function isSpecial( $name ) { 00802 if ( $this->isSpecialPage() ) { 00803 list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); 00804 if ( $name == $thisName ) { 00805 return true; 00806 } 00807 } 00808 return false; 00809 } 00810 00817 public function fixSpecialName() { 00818 if ( $this->isSpecialPage() ) { 00819 list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); 00820 if ( $canonicalName ) { 00821 $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); 00822 if ( $localName != $this->mDbkeyform ) { 00823 return Title::makeTitle( NS_SPECIAL, $localName ); 00824 } 00825 } 00826 } 00827 return $this; 00828 } 00829 00840 public function inNamespace( $ns ) { 00841 return MWNamespace::equals( $this->getNamespace(), $ns ); 00842 } 00843 00851 public function inNamespaces( /* ... */ ) { 00852 $namespaces = func_get_args(); 00853 if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { 00854 $namespaces = $namespaces[0]; 00855 } 00856 00857 foreach ( $namespaces as $ns ) { 00858 if ( $this->inNamespace( $ns ) ) { 00859 return true; 00860 } 00861 } 00862 00863 return false; 00864 } 00865 00879 public function hasSubjectNamespace( $ns ) { 00880 return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); 00881 } 00882 00890 public function isContentPage() { 00891 return MWNamespace::isContent( $this->getNamespace() ); 00892 } 00893 00900 public function isMovable() { 00901 if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) { 00902 // Interwiki title or immovable namespace. Hooks don't get to override here 00903 return false; 00904 } 00905 00906 $result = true; 00907 wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); 00908 return $result; 00909 } 00910 00921 public function isMainPage() { 00922 return $this->equals( Title::newMainPage() ); 00923 } 00924 00930 public function isSubpage() { 00931 return MWNamespace::hasSubpages( $this->mNamespace ) 00932 ? strpos( $this->getText(), '/' ) !== false 00933 : false; 00934 } 00935 00941 public function isConversionTable() { 00942 //@todo: ConversionTable should become a separate content model. 00943 00944 return $this->getNamespace() == NS_MEDIAWIKI && 00945 strpos( $this->getText(), 'Conversiontable/' ) === 0; 00946 } 00947 00953 public function isWikitextPage() { 00954 return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); 00955 } 00956 00968 public function isCssOrJsPage() { 00969 $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace 00970 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 00971 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 00972 00973 #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure 00974 # hook functions can force this method to return true even outside the mediawiki namespace. 00975 00976 wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) ); 00977 00978 return $isCssOrJsPage; 00979 } 00980 00985 public function isCssJsSubpage() { 00986 return ( NS_USER == $this->mNamespace && $this->isSubpage() 00987 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 00988 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); 00989 } 00990 00996 public function getSkinFromCssJsSubpage() { 00997 $subpage = explode( '/', $this->mTextform ); 00998 $subpage = $subpage[ count( $subpage ) - 1 ]; 00999 $lastdot = strrpos( $subpage, '.' ); 01000 if ( $lastdot === false ) 01001 return $subpage; # Never happens: only called for names ending in '.css' or '.js' 01002 return substr( $subpage, 0, $lastdot ); 01003 } 01004 01010 public function isCssSubpage() { 01011 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01012 && $this->hasContentModel( CONTENT_MODEL_CSS ) ); 01013 } 01014 01020 public function isJsSubpage() { 01021 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01022 && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01023 } 01024 01030 public function isTalkPage() { 01031 return MWNamespace::isTalk( $this->getNamespace() ); 01032 } 01033 01039 public function getTalkPage() { 01040 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); 01041 } 01042 01049 public function getSubjectPage() { 01050 // Is this the same title? 01051 $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); 01052 if ( $this->getNamespace() == $subjectNS ) { 01053 return $this; 01054 } 01055 return Title::makeTitle( $subjectNS, $this->getDBkey() ); 01056 } 01057 01063 public function getDefaultNamespace() { 01064 return $this->mDefaultNamespace; 01065 } 01066 01073 public function getIndexTitle() { 01074 return Title::indexTitle( $this->mNamespace, $this->mTextform ); 01075 } 01076 01082 public function getFragment() { 01083 return $this->mFragment; 01084 } 01085 01090 public function getFragmentForURL() { 01091 if ( $this->mFragment == '' ) { 01092 return ''; 01093 } else { 01094 return '#' . Title::escapeFragmentForURL( $this->mFragment ); 01095 } 01096 } 01097 01108 public function setFragment( $fragment ) { 01109 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); 01110 } 01111 01120 private function prefix( $name ) { 01121 $p = ''; 01122 if ( $this->mInterwiki != '' ) { 01123 $p = $this->mInterwiki . ':'; 01124 } 01125 01126 if ( 0 != $this->mNamespace ) { 01127 $p .= $this->getNsText() . ':'; 01128 } 01129 return $p . $name; 01130 } 01131 01138 public function getPrefixedDBkey() { 01139 $s = $this->prefix( $this->mDbkeyform ); 01140 $s = str_replace( ' ', '_', $s ); 01141 return $s; 01142 } 01143 01150 public function getPrefixedText() { 01151 // @todo FIXME: Bad usage of empty() ? 01152 if ( empty( $this->mPrefixedText ) ) { 01153 $s = $this->prefix( $this->mTextform ); 01154 $s = str_replace( '_', ' ', $s ); 01155 $this->mPrefixedText = $s; 01156 } 01157 return $this->mPrefixedText; 01158 } 01159 01165 public function __toString() { 01166 return $this->getPrefixedText(); 01167 } 01168 01175 public function getFullText() { 01176 $text = $this->getPrefixedText(); 01177 if ( $this->mFragment != '' ) { 01178 $text .= '#' . $this->mFragment; 01179 } 01180 return $text; 01181 } 01182 01195 public function getRootText() { 01196 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01197 return $this->getText(); 01198 } 01199 01200 return strtok( $this->getText(), '/' ); 01201 } 01202 01215 public function getRootTitle() { 01216 return Title::makeTitle( $this->getNamespace(), $this->getRootText() ); 01217 } 01218 01230 public function getBaseText() { 01231 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01232 return $this->getText(); 01233 } 01234 01235 $parts = explode( '/', $this->getText() ); 01236 # Don't discard the real title if there's no subpage involved 01237 if ( count( $parts ) > 1 ) { 01238 unset( $parts[count( $parts ) - 1] ); 01239 } 01240 return implode( '/', $parts ); 01241 } 01242 01255 public function getBaseTitle() { 01256 return Title::makeTitle( $this->getNamespace(), $this->getBaseText() ); 01257 } 01258 01270 public function getSubpageText() { 01271 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01272 return( $this->mTextform ); 01273 } 01274 $parts = explode( '/', $this->mTextform ); 01275 return( $parts[count( $parts ) - 1] ); 01276 } 01277 01291 public function getSubpage( $text ) { 01292 return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text ); 01293 } 01294 01302 public function getEscapedText() { 01303 wfDeprecated( __METHOD__, '1.19' ); 01304 return htmlspecialchars( $this->getPrefixedText() ); 01305 } 01306 01312 public function getSubpageUrlForm() { 01313 $text = $this->getSubpageText(); 01314 $text = wfUrlencode( str_replace( ' ', '_', $text ) ); 01315 return( $text ); 01316 } 01317 01323 public function getPrefixedURL() { 01324 $s = $this->prefix( $this->mDbkeyform ); 01325 $s = wfUrlencode( str_replace( ' ', '_', $s ) ); 01326 return $s; 01327 } 01328 01342 private static function fixUrlQueryArgs( $query, $query2 = false ) { 01343 if( $query2 !== false ) { 01344 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" ); 01345 } 01346 if ( is_array( $query ) ) { 01347 $query = wfArrayToCgi( $query ); 01348 } 01349 if ( $query2 ) { 01350 if ( is_string( $query2 ) ) { 01351 // $query2 is a string, we will consider this to be 01352 // a deprecated $variant argument and add it to the query 01353 $query2 = wfArrayToCgi( array( 'variant' => $query2 ) ); 01354 } else { 01355 $query2 = wfArrayToCgi( $query2 ); 01356 } 01357 // If we have $query content add a & to it first 01358 if ( $query ) { 01359 $query .= '&'; 01360 } 01361 // Now append the queries together 01362 $query .= $query2; 01363 } 01364 return $query; 01365 } 01366 01380 public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01381 $query = self::fixUrlQueryArgs( $query, $query2 ); 01382 01383 # Hand off all the decisions on urls to getLocalURL 01384 $url = $this->getLocalURL( $query ); 01385 01386 # Expand the url to make it a full url. Note that getLocalURL has the 01387 # potential to output full urls for a variety of reasons, so we use 01388 # wfExpandUrl instead of simply prepending $wgServer 01389 $url = wfExpandUrl( $url, $proto ); 01390 01391 # Finally, add the fragment. 01392 $url .= $this->getFragmentForURL(); 01393 01394 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); 01395 return $url; 01396 } 01397 01416 public function getLocalURL( $query = '', $query2 = false ) { 01417 global $wgArticlePath, $wgScript, $wgServer, $wgRequest; 01418 01419 $query = self::fixUrlQueryArgs( $query, $query2 ); 01420 01421 $interwiki = Interwiki::fetch( $this->mInterwiki ); 01422 if ( $interwiki ) { 01423 $namespace = $this->getNsText(); 01424 if ( $namespace != '' ) { 01425 # Can this actually happen? Interwikis shouldn't be parsed. 01426 # Yes! It can in interwiki transclusion. But... it probably shouldn't. 01427 $namespace .= ':'; 01428 } 01429 $url = $interwiki->getURL( $namespace . $this->getDBkey() ); 01430 $url = wfAppendQuery( $url, $query ); 01431 } else { 01432 $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); 01433 if ( $query == '' ) { 01434 $url = str_replace( '$1', $dbkey, $wgArticlePath ); 01435 wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); 01436 } else { 01437 global $wgVariantArticlePath, $wgActionPaths; 01438 $url = false; 01439 $matches = array(); 01440 01441 if ( !empty( $wgActionPaths ) && 01442 preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) 01443 { 01444 $action = urldecode( $matches[2] ); 01445 if ( isset( $wgActionPaths[$action] ) ) { 01446 $query = $matches[1]; 01447 if ( isset( $matches[4] ) ) { 01448 $query .= $matches[4]; 01449 } 01450 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); 01451 if ( $query != '' ) { 01452 $url = wfAppendQuery( $url, $query ); 01453 } 01454 } 01455 } 01456 01457 if ( $url === false && 01458 $wgVariantArticlePath && 01459 $this->getPageLanguage()->hasVariants() && 01460 preg_match( '/^variant=([^&]*)$/', $query, $matches ) ) 01461 { 01462 $variant = urldecode( $matches[1] ); 01463 if ( $this->getPageLanguage()->hasVariant( $variant ) ) { 01464 // Only do the variant replacement if the given variant is a valid 01465 // variant for the page's language. 01466 $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); 01467 $url = str_replace( '$1', $dbkey, $url ); 01468 } 01469 } 01470 01471 if ( $url === false ) { 01472 if ( $query == '-' ) { 01473 $query = ''; 01474 } 01475 $url = "{$wgScript}?title={$dbkey}&{$query}"; 01476 } 01477 } 01478 01479 wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) ); 01480 01481 // @todo FIXME: This causes breakage in various places when we 01482 // actually expected a local URL and end up with dupe prefixes. 01483 if ( $wgRequest->getVal( 'action' ) == 'render' ) { 01484 $url = $wgServer . $url; 01485 } 01486 } 01487 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) ); 01488 return $url; 01489 } 01490 01509 public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01510 wfProfileIn( __METHOD__ ); 01511 if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) { 01512 $ret = $this->getFullURL( $query, $query2, $proto ); 01513 } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { 01514 $ret = $this->getFragmentForURL(); 01515 } else { 01516 $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL(); 01517 } 01518 wfProfileOut( __METHOD__ ); 01519 return $ret; 01520 } 01521 01534 public function escapeLocalURL( $query = '', $query2 = false ) { 01535 wfDeprecated( __METHOD__, '1.19' ); 01536 return htmlspecialchars( $this->getLocalURL( $query, $query2 ) ); 01537 } 01538 01549 public function escapeFullURL( $query = '', $query2 = false ) { 01550 wfDeprecated( __METHOD__, '1.19' ); 01551 return htmlspecialchars( $this->getFullURL( $query, $query2 ) ); 01552 } 01553 01568 public function getInternalURL( $query = '', $query2 = false ) { 01569 global $wgInternalServer, $wgServer; 01570 $query = self::fixUrlQueryArgs( $query, $query2 ); 01571 $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; 01572 $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP ); 01573 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); 01574 return $url; 01575 } 01576 01590 public function getCanonicalURL( $query = '', $query2 = false ) { 01591 $query = self::fixUrlQueryArgs( $query, $query2 ); 01592 $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL ); 01593 wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) ); 01594 return $url; 01595 } 01596 01607 public function escapeCanonicalURL( $query = '', $query2 = false ) { 01608 wfDeprecated( __METHOD__, '1.19' ); 01609 return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) ); 01610 } 01611 01618 public function getEditURL() { 01619 if ( $this->mInterwiki != '' ) { 01620 return ''; 01621 } 01622 $s = $this->getLocalURL( 'action=edit' ); 01623 01624 return $s; 01625 } 01626 01633 public function userIsWatching() { 01634 global $wgUser; 01635 01636 if ( is_null( $this->mWatched ) ) { 01637 if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) { 01638 $this->mWatched = false; 01639 } else { 01640 $this->mWatched = $wgUser->isWatched( $this ); 01641 } 01642 } 01643 return $this->mWatched; 01644 } 01645 01653 public function userCanRead() { 01654 wfDeprecated( __METHOD__, '1.19' ); 01655 return $this->userCan( 'read' ); 01656 } 01657 01673 public function quickUserCan( $action, $user = null ) { 01674 return $this->userCan( $action, $user, false ); 01675 } 01676 01687 public function userCan( $action, $user = null, $doExpensiveQueries = true ) { 01688 if ( !$user instanceof User ) { 01689 global $wgUser; 01690 $user = $wgUser; 01691 } 01692 return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) ); 01693 } 01694 01708 public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { 01709 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); 01710 01711 // Remove the errors being ignored. 01712 foreach ( $errors as $index => $error ) { 01713 $error_key = is_array( $error ) ? $error[0] : $error; 01714 01715 if ( in_array( $error_key, $ignoreErrors ) ) { 01716 unset( $errors[$index] ); 01717 } 01718 } 01719 01720 return $errors; 01721 } 01722 01734 private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01735 if ( $action == 'create' ) { 01736 if ( 01737 ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || 01738 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) 01739 ) { 01740 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); 01741 } 01742 } elseif ( $action == 'move' ) { 01743 if ( !$user->isAllowed( 'move-rootuserpages' ) 01744 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01745 // Show user page-specific message only if the user can move other pages 01746 $errors[] = array( 'cant-move-user-page' ); 01747 } 01748 01749 // Check if user is allowed to move files if it's a file 01750 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { 01751 $errors[] = array( 'movenotallowedfile' ); 01752 } 01753 01754 if ( !$user->isAllowed( 'move' ) ) { 01755 // User can't move anything 01756 $userCanMove = User::groupHasPermission( 'user', 'move' ); 01757 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); 01758 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { 01759 // custom message if logged-in users without any special rights can move 01760 $errors[] = array( 'movenologintext' ); 01761 } else { 01762 $errors[] = array( 'movenotallowed' ); 01763 } 01764 } 01765 } elseif ( $action == 'move-target' ) { 01766 if ( !$user->isAllowed( 'move' ) ) { 01767 // User can't move anything 01768 $errors[] = array( 'movenotallowed' ); 01769 } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 01770 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01771 // Show user page-specific message only if the user can move other pages 01772 $errors[] = array( 'cant-move-to-user-page' ); 01773 } 01774 } elseif ( !$user->isAllowed( $action ) ) { 01775 $errors[] = $this->missingPermissionError( $action, $short ); 01776 } 01777 01778 return $errors; 01779 } 01780 01789 private function resultToError( $errors, $result ) { 01790 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { 01791 // A single array representing an error 01792 $errors[] = $result; 01793 } elseif ( is_array( $result ) && is_array( $result[0] ) ) { 01794 // A nested array representing multiple errors 01795 $errors = array_merge( $errors, $result ); 01796 } elseif ( $result !== '' && is_string( $result ) ) { 01797 // A string representing a message-id 01798 $errors[] = array( $result ); 01799 } elseif ( $result === false ) { 01800 // a generic "We don't want them to do that" 01801 $errors[] = array( 'badaccess-group0' ); 01802 } 01803 return $errors; 01804 } 01805 01817 private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { 01818 // Use getUserPermissionsErrors instead 01819 $result = ''; 01820 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { 01821 return $result ? array() : array( array( 'badaccess-group0' ) ); 01822 } 01823 // Check getUserPermissionsErrors hook 01824 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { 01825 $errors = $this->resultToError( $errors, $result ); 01826 } 01827 // Check getUserPermissionsErrorsExpensive hook 01828 if ( 01829 $doExpensiveQueries 01830 && !( $short && count( $errors ) > 0 ) 01831 && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) 01832 ) { 01833 $errors = $this->resultToError( $errors, $result ); 01834 } 01835 01836 return $errors; 01837 } 01838 01850 private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01851 # Only 'createaccount' can be performed on special pages, 01852 # which don't actually exist in the DB. 01853 if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) { 01854 $errors[] = array( 'ns-specialprotected' ); 01855 } 01856 01857 # Check $wgNamespaceProtection for restricted namespaces 01858 if ( $this->isNamespaceProtected( $user ) ) { 01859 $ns = $this->mNamespace == NS_MAIN ? 01860 wfMessage( 'nstab-main' )->text() : $this->getNsText(); 01861 $errors[] = $this->mNamespace == NS_MEDIAWIKI ? 01862 array( 'protectedinterface' ) : array( 'namespaceprotected', $ns ); 01863 } 01864 01865 return $errors; 01866 } 01867 01879 private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01880 # Protect css/js subpages of user pages 01881 # XXX: this might be better using restrictions 01882 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only 01883 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) 01884 && !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { 01885 if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { 01886 $errors[] = array( 'customcssprotected' ); 01887 } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { 01888 $errors[] = array( 'customjsprotected' ); 01889 } 01890 } 01891 01892 return $errors; 01893 } 01894 01908 private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01909 foreach ( $this->getRestrictions( $action ) as $right ) { 01910 // Backwards compatibility, rewrite sysop -> protect 01911 if ( $right == 'sysop' ) { 01912 $right = 'protect'; 01913 } 01914 if ( $right != '' && !$user->isAllowed( $right ) ) { 01915 // Users with 'editprotected' permission can edit protected pages 01916 // without cascading option turned on. 01917 if ( $action != 'edit' || !$user->isAllowed( 'editprotected' ) 01918 || $this->mCascadeRestriction ) 01919 { 01920 $errors[] = array( 'protectedpagetext', $right ); 01921 } 01922 } 01923 } 01924 01925 return $errors; 01926 } 01927 01939 private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01940 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { 01941 # We /could/ use the protection level on the source page, but it's 01942 # fairly ugly as we have to establish a precedence hierarchy for pages 01943 # included by multiple cascade-protected pages. So just restrict 01944 # it to people with 'protect' permission, as they could remove the 01945 # protection anyway. 01946 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); 01947 # Cascading protection depends on more than this page... 01948 # Several cascading protected pages may include this page... 01949 # Check each cascading level 01950 # This is only for protection restrictions, not for all actions 01951 if ( isset( $restrictions[$action] ) ) { 01952 foreach ( $restrictions[$action] as $right ) { 01953 $right = ( $right == 'sysop' ) ? 'protect' : $right; 01954 if ( $right != '' && !$user->isAllowed( $right ) ) { 01955 $pages = ''; 01956 foreach ( $cascadingSources as $page ) 01957 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; 01958 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); 01959 } 01960 } 01961 } 01962 } 01963 01964 return $errors; 01965 } 01966 01978 private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01979 global $wgDeleteRevisionsLimit, $wgLang; 01980 01981 if ( $action == 'protect' ) { 01982 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) { 01983 // If they can't edit, they shouldn't protect. 01984 $errors[] = array( 'protect-cantedit' ); 01985 } 01986 } elseif ( $action == 'create' ) { 01987 $title_protection = $this->getTitleProtection(); 01988 if( $title_protection ) { 01989 if( $title_protection['pt_create_perm'] == 'sysop' ) { 01990 $title_protection['pt_create_perm'] = 'protect'; // B/C 01991 } 01992 if( $title_protection['pt_create_perm'] == '' || 01993 !$user->isAllowed( $title_protection['pt_create_perm'] ) ) 01994 { 01995 $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] ); 01996 } 01997 } 01998 } elseif ( $action == 'move' ) { 01999 // Check for immobile pages 02000 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02001 // Specific message for this case 02002 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 02003 } elseif ( !$this->isMovable() ) { 02004 // Less specific message for rarer cases 02005 $errors[] = array( 'immobile-source-page' ); 02006 } 02007 } elseif ( $action == 'move-target' ) { 02008 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02009 $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); 02010 } elseif ( !$this->isMovable() ) { 02011 $errors[] = array( 'immobile-target-page' ); 02012 } 02013 } elseif ( $action == 'delete' ) { 02014 if ( $doExpensiveQueries && $wgDeleteRevisionsLimit 02015 && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() ) 02016 { 02017 $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); 02018 } 02019 } 02020 return $errors; 02021 } 02022 02034 private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { 02035 // Account creation blocks handled at userlogin. 02036 // Unblocking handled in SpecialUnblock 02037 if( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { 02038 return $errors; 02039 } 02040 02041 global $wgContLang, $wgLang, $wgEmailConfirmToEdit; 02042 02043 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { 02044 $errors[] = array( 'confirmedittext' ); 02045 } 02046 02047 if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) { 02048 // Don't block the user from editing their own talk page unless they've been 02049 // explicitly blocked from that too. 02050 } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { 02051 $block = $user->getBlock(); 02052 02053 // This is from OutputPage::blockedPage 02054 // Copied at r23888 by werdna 02055 02056 $id = $user->blockedBy(); 02057 $reason = $user->blockedFor(); 02058 if ( $reason == '' ) { 02059 $reason = wfMessage( 'blockednoreason' )->text(); 02060 } 02061 $ip = $user->getRequest()->getIP(); 02062 02063 if ( is_numeric( $id ) ) { 02064 $name = User::whoIs( $id ); 02065 } else { 02066 $name = $id; 02067 } 02068 02069 $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; 02070 $blockid = $block->getId(); 02071 $blockExpiry = $block->getExpiry(); 02072 $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true ); 02073 if ( $blockExpiry == 'infinity' ) { 02074 $blockExpiry = wfMessage( 'infiniteblock' )->text(); 02075 } else { 02076 $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); 02077 } 02078 02079 $intended = strval( $block->getTarget() ); 02080 02081 $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name, 02082 $blockid, $blockExpiry, $intended, $blockTimestamp ); 02083 } 02084 02085 return $errors; 02086 } 02087 02099 private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02100 global $wgWhitelistRead, $wgWhitelistReadRegexp, $wgRevokePermissions; 02101 static $useShortcut = null; 02102 02103 # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below 02104 if ( is_null( $useShortcut ) ) { 02105 $useShortcut = true; 02106 if ( !User::groupHasPermission( '*', 'read' ) ) { 02107 # Not a public wiki, so no shortcut 02108 $useShortcut = false; 02109 } elseif ( !empty( $wgRevokePermissions ) ) { 02116 foreach ( $wgRevokePermissions as $perms ) { 02117 if ( !empty( $perms['read'] ) ) { 02118 # We might be removing the read right from the user, so no shortcut 02119 $useShortcut = false; 02120 break; 02121 } 02122 } 02123 } 02124 } 02125 02126 $whitelisted = false; 02127 if ( $useShortcut ) { 02128 # Shortcut for public wikis, allows skipping quite a bit of code 02129 $whitelisted = true; 02130 } elseif ( $user->isAllowed( 'read' ) ) { 02131 # If the user is allowed to read pages, he is allowed to read all pages 02132 $whitelisted = true; 02133 } elseif ( $this->isSpecial( 'Userlogin' ) 02134 || $this->isSpecial( 'ChangePassword' ) 02135 || $this->isSpecial( 'PasswordReset' ) 02136 ) { 02137 # Always grant access to the login page. 02138 # Even anons need to be able to log in. 02139 $whitelisted = true; 02140 } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { 02141 # Time to check the whitelist 02142 # Only do these checks is there's something to check against 02143 $name = $this->getPrefixedText(); 02144 $dbName = $this->getPrefixedDBkey(); 02145 02146 // Check for explicit whitelisting with and without underscores 02147 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { 02148 $whitelisted = true; 02149 } elseif ( $this->getNamespace() == NS_MAIN ) { 02150 # Old settings might have the title prefixed with 02151 # a colon for main-namespace pages 02152 if ( in_array( ':' . $name, $wgWhitelistRead ) ) { 02153 $whitelisted = true; 02154 } 02155 } elseif ( $this->isSpecialPage() ) { 02156 # If it's a special page, ditch the subpage bit and check again 02157 $name = $this->getDBkey(); 02158 list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); 02159 if ( $name ) { 02160 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); 02161 if ( in_array( $pure, $wgWhitelistRead, true ) ) { 02162 $whitelisted = true; 02163 } 02164 } 02165 } 02166 } 02167 02168 if( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { 02169 $name = $this->getPrefixedText(); 02170 // Check for regex whitelisting 02171 foreach ( $wgWhitelistReadRegexp as $listItem ) { 02172 if ( preg_match( $listItem, $name ) ) { 02173 $whitelisted = true; 02174 break; 02175 } 02176 } 02177 } 02178 02179 if ( !$whitelisted ) { 02180 # If the title is not whitelisted, give extensions a chance to do so... 02181 wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); 02182 if ( !$whitelisted ) { 02183 $errors[] = $this->missingPermissionError( $action, $short ); 02184 } 02185 } 02186 02187 return $errors; 02188 } 02189 02198 private function missingPermissionError( $action, $short ) { 02199 // We avoid expensive display logic for quickUserCan's and such 02200 if ( $short ) { 02201 return array( 'badaccess-group0' ); 02202 } 02203 02204 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), 02205 User::getGroupsWithPermission( $action ) ); 02206 02207 if ( count( $groups ) ) { 02208 global $wgLang; 02209 return array( 02210 'badaccess-groups', 02211 $wgLang->commaList( $groups ), 02212 count( $groups ) 02213 ); 02214 } else { 02215 return array( 'badaccess-group0' ); 02216 } 02217 } 02218 02230 protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { 02231 wfProfileIn( __METHOD__ ); 02232 02233 # Read has special handling 02234 if ( $action == 'read' ) { 02235 $checks = array( 02236 'checkPermissionHooks', 02237 'checkReadPermissions', 02238 ); 02239 } else { 02240 $checks = array( 02241 'checkQuickPermissions', 02242 'checkPermissionHooks', 02243 'checkSpecialsAndNSPermissions', 02244 'checkCSSandJSPermissions', 02245 'checkPageRestrictions', 02246 'checkCascadingSourcesRestrictions', 02247 'checkActionPermissions', 02248 'checkUserBlock' 02249 ); 02250 } 02251 02252 $errors = array(); 02253 while( count( $checks ) > 0 && 02254 !( $short && count( $errors ) > 0 ) ) { 02255 $method = array_shift( $checks ); 02256 $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); 02257 } 02258 02259 wfProfileOut( __METHOD__ ); 02260 return $errors; 02261 } 02262 02270 public function userCanEditCssSubpage() { 02271 global $wgUser; 02272 wfDeprecated( __METHOD__, '1.19' ); 02273 return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) ) 02274 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); 02275 } 02276 02284 public function userCanEditJsSubpage() { 02285 global $wgUser; 02286 wfDeprecated( __METHOD__, '1.19' ); 02287 return ( 02288 ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) ) 02289 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) 02290 ); 02291 } 02292 02300 public static function getFilteredRestrictionTypes( $exists = true ) { 02301 global $wgRestrictionTypes; 02302 $types = $wgRestrictionTypes; 02303 if ( $exists ) { 02304 # Remove the create restriction for existing titles 02305 $types = array_diff( $types, array( 'create' ) ); 02306 } else { 02307 # Only the create and upload restrictions apply to non-existing titles 02308 $types = array_intersect( $types, array( 'create', 'upload' ) ); 02309 } 02310 return $types; 02311 } 02312 02318 public function getRestrictionTypes() { 02319 if ( $this->isSpecialPage() ) { 02320 return array(); 02321 } 02322 02323 $types = self::getFilteredRestrictionTypes( $this->exists() ); 02324 02325 if ( $this->getNamespace() != NS_FILE ) { 02326 # Remove the upload restriction for non-file titles 02327 $types = array_diff( $types, array( 'upload' ) ); 02328 } 02329 02330 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); 02331 02332 wfDebug( __METHOD__ . ': applicable restrictions to [[' . 02333 $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); 02334 02335 return $types; 02336 } 02337 02345 private function getTitleProtection() { 02346 // Can't protect pages in special namespaces 02347 if ( $this->getNamespace() < 0 ) { 02348 return false; 02349 } 02350 02351 // Can't protect pages that exist. 02352 if ( $this->exists() ) { 02353 return false; 02354 } 02355 02356 if ( !isset( $this->mTitleProtection ) ) { 02357 $dbr = wfGetDB( DB_SLAVE ); 02358 $res = $dbr->select( 02359 'protected_titles', 02360 array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ), 02361 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02362 __METHOD__ 02363 ); 02364 02365 // fetchRow returns false if there are no rows. 02366 $this->mTitleProtection = $dbr->fetchRow( $res ); 02367 } 02368 return $this->mTitleProtection; 02369 } 02370 02380 public function updateTitleProtection( $create_perm, $reason, $expiry ) { 02381 wfDeprecated( __METHOD__, '1.19' ); 02382 02383 global $wgUser; 02384 02385 $limit = array( 'create' => $create_perm ); 02386 $expiry = array( 'create' => $expiry ); 02387 02388 $page = WikiPage::factory( $this ); 02389 $cascade = false; 02390 $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $wgUser ); 02391 02392 return $status->isOK(); 02393 } 02394 02398 public function deleteTitleProtection() { 02399 $dbw = wfGetDB( DB_MASTER ); 02400 02401 $dbw->delete( 02402 'protected_titles', 02403 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02404 __METHOD__ 02405 ); 02406 $this->mTitleProtection = false; 02407 } 02408 02415 public function isSemiProtected( $action = 'edit' ) { 02416 if ( $this->exists() ) { 02417 $restrictions = $this->getRestrictions( $action ); 02418 if ( count( $restrictions ) > 0 ) { 02419 foreach ( $restrictions as $restriction ) { 02420 if ( strtolower( $restriction ) != 'autoconfirmed' ) { 02421 return false; 02422 } 02423 } 02424 } else { 02425 # Not protected 02426 return false; 02427 } 02428 return true; 02429 } else { 02430 # If it doesn't exist, it can't be protected 02431 return false; 02432 } 02433 } 02434 02442 public function isProtected( $action = '' ) { 02443 global $wgRestrictionLevels; 02444 02445 $restrictionTypes = $this->getRestrictionTypes(); 02446 02447 # Special pages have inherent protection 02448 if( $this->isSpecialPage() ) { 02449 return true; 02450 } 02451 02452 # Check regular protection levels 02453 foreach ( $restrictionTypes as $type ) { 02454 if ( $action == $type || $action == '' ) { 02455 $r = $this->getRestrictions( $type ); 02456 foreach ( $wgRestrictionLevels as $level ) { 02457 if ( in_array( $level, $r ) && $level != '' ) { 02458 return true; 02459 } 02460 } 02461 } 02462 } 02463 02464 return false; 02465 } 02466 02474 public function isNamespaceProtected( User $user ) { 02475 global $wgNamespaceProtection; 02476 02477 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { 02478 foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { 02479 if ( $right != '' && !$user->isAllowed( $right ) ) { 02480 return true; 02481 } 02482 } 02483 } 02484 return false; 02485 } 02486 02492 public function isCascadeProtected() { 02493 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); 02494 return ( $sources > 0 ); 02495 } 02496 02507 public function getCascadeProtectionSources( $getPages = true ) { 02508 global $wgContLang; 02509 $pagerestrictions = array(); 02510 02511 if ( isset( $this->mCascadeSources ) && $getPages ) { 02512 return array( $this->mCascadeSources, $this->mCascadingRestrictions ); 02513 } elseif ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) { 02514 return array( $this->mHasCascadingRestrictions, $pagerestrictions ); 02515 } 02516 02517 wfProfileIn( __METHOD__ ); 02518 02519 $dbr = wfGetDB( DB_SLAVE ); 02520 02521 if ( $this->getNamespace() == NS_FILE ) { 02522 $tables = array( 'imagelinks', 'page_restrictions' ); 02523 $where_clauses = array( 02524 'il_to' => $this->getDBkey(), 02525 'il_from=pr_page', 02526 'pr_cascade' => 1 02527 ); 02528 } else { 02529 $tables = array( 'templatelinks', 'page_restrictions' ); 02530 $where_clauses = array( 02531 'tl_namespace' => $this->getNamespace(), 02532 'tl_title' => $this->getDBkey(), 02533 'tl_from=pr_page', 02534 'pr_cascade' => 1 02535 ); 02536 } 02537 02538 if ( $getPages ) { 02539 $cols = array( 'pr_page', 'page_namespace', 'page_title', 02540 'pr_expiry', 'pr_type', 'pr_level' ); 02541 $where_clauses[] = 'page_id=pr_page'; 02542 $tables[] = 'page'; 02543 } else { 02544 $cols = array( 'pr_expiry' ); 02545 } 02546 02547 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); 02548 02549 $sources = $getPages ? array() : false; 02550 $now = wfTimestampNow(); 02551 $purgeExpired = false; 02552 02553 foreach ( $res as $row ) { 02554 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02555 if ( $expiry > $now ) { 02556 if ( $getPages ) { 02557 $page_id = $row->pr_page; 02558 $page_ns = $row->page_namespace; 02559 $page_title = $row->page_title; 02560 $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); 02561 # Add groups needed for each restriction type if its not already there 02562 # Make sure this restriction type still exists 02563 02564 if ( !isset( $pagerestrictions[$row->pr_type] ) ) { 02565 $pagerestrictions[$row->pr_type] = array(); 02566 } 02567 02568 if ( 02569 isset( $pagerestrictions[$row->pr_type] ) 02570 && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) 02571 ) { 02572 $pagerestrictions[$row->pr_type][] = $row->pr_level; 02573 } 02574 } else { 02575 $sources = true; 02576 } 02577 } else { 02578 // Trigger lazy purge of expired restrictions from the db 02579 $purgeExpired = true; 02580 } 02581 } 02582 if ( $purgeExpired ) { 02583 Title::purgeExpiredRestrictions(); 02584 } 02585 02586 if ( $getPages ) { 02587 $this->mCascadeSources = $sources; 02588 $this->mCascadingRestrictions = $pagerestrictions; 02589 } else { 02590 $this->mHasCascadingRestrictions = $sources; 02591 } 02592 02593 wfProfileOut( __METHOD__ ); 02594 return array( $sources, $pagerestrictions ); 02595 } 02596 02603 public function getRestrictions( $action ) { 02604 if ( !$this->mRestrictionsLoaded ) { 02605 $this->loadRestrictions(); 02606 } 02607 return isset( $this->mRestrictions[$action] ) 02608 ? $this->mRestrictions[$action] 02609 : array(); 02610 } 02611 02619 public function getRestrictionExpiry( $action ) { 02620 if ( !$this->mRestrictionsLoaded ) { 02621 $this->loadRestrictions(); 02622 } 02623 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; 02624 } 02625 02631 function areRestrictionsCascading() { 02632 if ( !$this->mRestrictionsLoaded ) { 02633 $this->loadRestrictions(); 02634 } 02635 02636 return $this->mCascadeRestriction; 02637 } 02638 02646 private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { 02647 $rows = array(); 02648 02649 foreach ( $res as $row ) { 02650 $rows[] = $row; 02651 } 02652 02653 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); 02654 } 02655 02665 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { 02666 global $wgContLang; 02667 $dbr = wfGetDB( DB_SLAVE ); 02668 02669 $restrictionTypes = $this->getRestrictionTypes(); 02670 02671 foreach ( $restrictionTypes as $type ) { 02672 $this->mRestrictions[$type] = array(); 02673 $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); 02674 } 02675 02676 $this->mCascadeRestriction = false; 02677 02678 # Backwards-compatibility: also load the restrictions from the page record (old format). 02679 02680 if ( $oldFashionedRestrictions === null ) { 02681 $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', 02682 array( 'page_id' => $this->getArticleID() ), __METHOD__ ); 02683 } 02684 02685 if ( $oldFashionedRestrictions != '' ) { 02686 02687 foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { 02688 $temp = explode( '=', trim( $restrict ) ); 02689 if ( count( $temp ) == 1 ) { 02690 // old old format should be treated as edit/move restriction 02691 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); 02692 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); 02693 } else { 02694 $restriction = trim( $temp[1] ); 02695 if( $restriction != '' ) { //some old entries are empty 02696 $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); 02697 } 02698 } 02699 } 02700 02701 $this->mOldRestrictions = true; 02702 02703 } 02704 02705 if ( count( $rows ) ) { 02706 # Current system - load second to make them override. 02707 $now = wfTimestampNow(); 02708 $purgeExpired = false; 02709 02710 # Cycle through all the restrictions. 02711 foreach ( $rows as $row ) { 02712 02713 // Don't take care of restrictions types that aren't allowed 02714 if ( !in_array( $row->pr_type, $restrictionTypes ) ) 02715 continue; 02716 02717 // This code should be refactored, now that it's being used more generally, 02718 // But I don't really see any harm in leaving it in Block for now -werdna 02719 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02720 02721 // Only apply the restrictions if they haven't expired! 02722 if ( !$expiry || $expiry > $now ) { 02723 $this->mRestrictionsExpiry[$row->pr_type] = $expiry; 02724 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); 02725 02726 $this->mCascadeRestriction |= $row->pr_cascade; 02727 } else { 02728 // Trigger a lazy purge of expired restrictions 02729 $purgeExpired = true; 02730 } 02731 } 02732 02733 if ( $purgeExpired ) { 02734 Title::purgeExpiredRestrictions(); 02735 } 02736 } 02737 02738 $this->mRestrictionsLoaded = true; 02739 } 02740 02747 public function loadRestrictions( $oldFashionedRestrictions = null ) { 02748 global $wgContLang; 02749 if ( !$this->mRestrictionsLoaded ) { 02750 if ( $this->exists() ) { 02751 $dbr = wfGetDB( DB_SLAVE ); 02752 02753 $res = $dbr->select( 02754 'page_restrictions', 02755 array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ), 02756 array( 'pr_page' => $this->getArticleID() ), 02757 __METHOD__ 02758 ); 02759 02760 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); 02761 } else { 02762 $title_protection = $this->getTitleProtection(); 02763 02764 if ( $title_protection ) { 02765 $now = wfTimestampNow(); 02766 $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); 02767 02768 if ( !$expiry || $expiry > $now ) { 02769 // Apply the restrictions 02770 $this->mRestrictionsExpiry['create'] = $expiry; 02771 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); 02772 } else { // Get rid of the old restrictions 02773 Title::purgeExpiredRestrictions(); 02774 $this->mTitleProtection = false; 02775 } 02776 } else { 02777 $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); 02778 } 02779 $this->mRestrictionsLoaded = true; 02780 } 02781 } 02782 } 02783 02788 public function flushRestrictions() { 02789 $this->mRestrictionsLoaded = false; 02790 $this->mTitleProtection = null; 02791 } 02792 02796 static function purgeExpiredRestrictions() { 02797 if ( wfReadOnly() ) { 02798 return; 02799 } 02800 02801 $dbw = wfGetDB( DB_MASTER ); 02802 $dbw->delete( 02803 'page_restrictions', 02804 array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02805 __METHOD__ 02806 ); 02807 02808 $dbw->delete( 02809 'protected_titles', 02810 array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02811 __METHOD__ 02812 ); 02813 } 02814 02820 public function hasSubpages() { 02821 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 02822 # Duh 02823 return false; 02824 } 02825 02826 # We dynamically add a member variable for the purpose of this method 02827 # alone to cache the result. There's no point in having it hanging 02828 # around uninitialized in every Title object; therefore we only add it 02829 # if needed and don't declare it statically. 02830 if ( isset( $this->mHasSubpages ) ) { 02831 return $this->mHasSubpages; 02832 } 02833 02834 $subpages = $this->getSubpages( 1 ); 02835 if ( $subpages instanceof TitleArray ) { 02836 return $this->mHasSubpages = (bool)$subpages->count(); 02837 } 02838 return $this->mHasSubpages = false; 02839 } 02840 02848 public function getSubpages( $limit = -1 ) { 02849 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 02850 return array(); 02851 } 02852 02853 $dbr = wfGetDB( DB_SLAVE ); 02854 $conds['page_namespace'] = $this->getNamespace(); 02855 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); 02856 $options = array(); 02857 if ( $limit > -1 ) { 02858 $options['LIMIT'] = $limit; 02859 } 02860 return $this->mSubpages = TitleArray::newFromResult( 02861 $dbr->select( 'page', 02862 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), 02863 $conds, 02864 __METHOD__, 02865 $options 02866 ) 02867 ); 02868 } 02869 02875 public function isDeleted() { 02876 if ( $this->getNamespace() < 0 ) { 02877 $n = 0; 02878 } else { 02879 $dbr = wfGetDB( DB_SLAVE ); 02880 02881 $n = $dbr->selectField( 'archive', 'COUNT(*)', 02882 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02883 __METHOD__ 02884 ); 02885 if ( $this->getNamespace() == NS_FILE ) { 02886 $n += $dbr->selectField( 'filearchive', 'COUNT(*)', 02887 array( 'fa_name' => $this->getDBkey() ), 02888 __METHOD__ 02889 ); 02890 } 02891 } 02892 return (int)$n; 02893 } 02894 02900 public function isDeletedQuick() { 02901 if ( $this->getNamespace() < 0 ) { 02902 return false; 02903 } 02904 $dbr = wfGetDB( DB_SLAVE ); 02905 $deleted = (bool)$dbr->selectField( 'archive', '1', 02906 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02907 __METHOD__ 02908 ); 02909 if ( !$deleted && $this->getNamespace() == NS_FILE ) { 02910 $deleted = (bool)$dbr->selectField( 'filearchive', '1', 02911 array( 'fa_name' => $this->getDBkey() ), 02912 __METHOD__ 02913 ); 02914 } 02915 return $deleted; 02916 } 02917 02926 public function getArticleID( $flags = 0 ) { 02927 if ( $this->getNamespace() < 0 ) { 02928 return $this->mArticleID = 0; 02929 } 02930 $linkCache = LinkCache::singleton(); 02931 if ( $flags & self::GAID_FOR_UPDATE ) { 02932 $oldUpdate = $linkCache->forUpdate( true ); 02933 $linkCache->clearLink( $this ); 02934 $this->mArticleID = $linkCache->addLinkObj( $this ); 02935 $linkCache->forUpdate( $oldUpdate ); 02936 } else { 02937 if ( -1 == $this->mArticleID ) { 02938 $this->mArticleID = $linkCache->addLinkObj( $this ); 02939 } 02940 } 02941 return $this->mArticleID; 02942 } 02943 02951 public function isRedirect( $flags = 0 ) { 02952 if ( !is_null( $this->mRedirect ) ) { 02953 return $this->mRedirect; 02954 } 02955 # Calling getArticleID() loads the field from cache as needed 02956 if ( !$this->getArticleID( $flags ) ) { 02957 return $this->mRedirect = false; 02958 } 02959 02960 $linkCache = LinkCache::singleton(); 02961 $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' ); 02962 if ( $cached === null ) { 02963 // TODO: check the assumption that the cache actually knows about this title 02964 // and handle this, such as get the title from the database. 02965 // See https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 02966 wfDebug( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); 02967 wfDebug( wfBacktrace() ); 02968 } 02969 02970 $this->mRedirect = (bool)$cached; 02971 02972 return $this->mRedirect; 02973 } 02974 02982 public function getLength( $flags = 0 ) { 02983 if ( $this->mLength != -1 ) { 02984 return $this->mLength; 02985 } 02986 # Calling getArticleID() loads the field from cache as needed 02987 if ( !$this->getArticleID( $flags ) ) { 02988 return $this->mLength = 0; 02989 } 02990 $linkCache = LinkCache::singleton(); 02991 $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' ); 02992 if ( $cached === null ) { # check the assumption that the cache actually knows about this title 02993 # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 02994 # as a stop gap, perhaps log this, but don't throw an exception? 02995 wfDebug( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); 02996 wfDebug( wfBacktrace() ); 02997 } 02998 02999 $this->mLength = intval( $cached ); 03000 03001 return $this->mLength; 03002 } 03003 03011 public function getLatestRevID( $flags = 0 ) { 03012 if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) { 03013 return intval( $this->mLatestID ); 03014 } 03015 # Calling getArticleID() loads the field from cache as needed 03016 if ( !$this->getArticleID( $flags ) ) { 03017 return $this->mLatestID = 0; 03018 } 03019 $linkCache = LinkCache::singleton(); 03020 $linkCache->addLinkObj( $this ); 03021 $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' ); 03022 if ( $cached === null ) { # check the assumption that the cache actually knows about this title 03023 # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209 03024 # as a stop gap, perhaps log this, but don't throw an exception? 03025 throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() ); 03026 } 03027 03028 $this->mLatestID = intval( $cached ); 03029 03030 return $this->mLatestID; 03031 } 03032 03043 public function resetArticleID( $newid ) { 03044 $linkCache = LinkCache::singleton(); 03045 $linkCache->clearLink( $this ); 03046 03047 if ( $newid === false ) { 03048 $this->mArticleID = -1; 03049 } else { 03050 $this->mArticleID = intval( $newid ); 03051 } 03052 $this->mRestrictionsLoaded = false; 03053 $this->mRestrictions = array(); 03054 $this->mRedirect = null; 03055 $this->mLength = -1; 03056 $this->mLatestID = false; 03057 $this->mContentModel = false; 03058 $this->mEstimateRevisions = null; 03059 } 03060 03068 public static function capitalize( $text, $ns = NS_MAIN ) { 03069 global $wgContLang; 03070 03071 if ( MWNamespace::isCapitalized( $ns ) ) { 03072 return $wgContLang->ucfirst( $text ); 03073 } else { 03074 return $text; 03075 } 03076 } 03077 03089 private function secureAndSplit() { 03090 global $wgContLang, $wgLocalInterwiki; 03091 03092 # Initialisation 03093 $this->mInterwiki = $this->mFragment = ''; 03094 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN 03095 03096 $dbkey = $this->mDbkeyform; 03097 03098 # Strip Unicode bidi override characters. 03099 # Sometimes they slip into cut-n-pasted page titles, where the 03100 # override chars get included in list displays. 03101 $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey ); 03102 03103 # Clean up whitespace 03104 # Note: use of the /u option on preg_replace here will cause 03105 # input with invalid UTF-8 sequences to be nullified out in PHP 5.2.x, 03106 # conveniently disabling them. 03107 $dbkey = preg_replace( '/[ _\xA0\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u', '_', $dbkey ); 03108 $dbkey = trim( $dbkey, '_' ); 03109 03110 if ( $dbkey == '' ) { 03111 return false; 03112 } 03113 03114 if ( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) { 03115 # Contained illegal UTF-8 sequences or forbidden Unicode chars. 03116 return false; 03117 } 03118 03119 $this->mDbkeyform = $dbkey; 03120 03121 # Initial colon indicates main namespace rather than specified default 03122 # but should not create invalid {ns,title} pairs such as {0,Project:Foo} 03123 if ( ':' == $dbkey[0] ) { 03124 $this->mNamespace = NS_MAIN; 03125 $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing 03126 $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace 03127 } 03128 03129 # Namespace or interwiki prefix 03130 $firstPass = true; 03131 $prefixRegexp = "/^(.+?)_*:_*(.*)$/S"; 03132 do { 03133 $m = array(); 03134 if ( preg_match( $prefixRegexp, $dbkey, $m ) ) { 03135 $p = $m[1]; 03136 if ( ( $ns = $wgContLang->getNsIndex( $p ) ) !== false ) { 03137 # Ordinary namespace 03138 $dbkey = $m[2]; 03139 $this->mNamespace = $ns; 03140 # For Talk:X pages, check if X has a "namespace" prefix 03141 if ( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) { 03142 if ( $wgContLang->getNsIndex( $x[1] ) ) { 03143 # Disallow Talk:File:x type titles... 03144 return false; 03145 } elseif ( Interwiki::isValidInterwiki( $x[1] ) ) { 03146 # Disallow Talk:Interwiki:x type titles... 03147 return false; 03148 } 03149 } 03150 } elseif ( Interwiki::isValidInterwiki( $p ) ) { 03151 if ( !$firstPass ) { 03152 # Can't make a local interwiki link to an interwiki link. 03153 # That's just crazy! 03154 return false; 03155 } 03156 03157 # Interwiki link 03158 $dbkey = $m[2]; 03159 $this->mInterwiki = $wgContLang->lc( $p ); 03160 03161 # Redundant interwiki prefix to the local wiki 03162 if ( $wgLocalInterwiki !== false 03163 && 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) 03164 { 03165 if ( $dbkey == '' ) { 03166 # Can't have an empty self-link 03167 return false; 03168 } 03169 $this->mInterwiki = ''; 03170 $firstPass = false; 03171 # Do another namespace split... 03172 continue; 03173 } 03174 03175 # If there's an initial colon after the interwiki, that also 03176 # resets the default namespace 03177 if ( $dbkey !== '' && $dbkey[0] == ':' ) { 03178 $this->mNamespace = NS_MAIN; 03179 $dbkey = substr( $dbkey, 1 ); 03180 } 03181 } 03182 # If there's no recognized interwiki or namespace, 03183 # then let the colon expression be part of the title. 03184 } 03185 break; 03186 } while ( true ); 03187 03188 # We already know that some pages won't be in the database! 03189 if ( $this->mInterwiki != '' || NS_SPECIAL == $this->mNamespace ) { 03190 $this->mArticleID = 0; 03191 } 03192 $fragment = strstr( $dbkey, '#' ); 03193 if ( false !== $fragment ) { 03194 $this->setFragment( $fragment ); 03195 $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) ); 03196 # remove whitespace again: prevents "Foo_bar_#" 03197 # becoming "Foo_bar_" 03198 $dbkey = preg_replace( '/_*$/', '', $dbkey ); 03199 } 03200 03201 # Reject illegal characters. 03202 $rxTc = self::getTitleInvalidRegex(); 03203 if ( preg_match( $rxTc, $dbkey ) ) { 03204 return false; 03205 } 03206 03207 # Pages with "/./" or "/../" appearing in the URLs will often be un- 03208 # reachable due to the way web browsers deal with 'relative' URLs. 03209 # Also, they conflict with subpage syntax. Forbid them explicitly. 03210 if ( 03211 strpos( $dbkey, '.' ) !== false && 03212 ( 03213 $dbkey === '.' || $dbkey === '..' || 03214 strpos( $dbkey, './' ) === 0 || 03215 strpos( $dbkey, '../' ) === 0 || 03216 strpos( $dbkey, '/./' ) !== false || 03217 strpos( $dbkey, '/../' ) !== false || 03218 substr( $dbkey, -2 ) == '/.' || 03219 substr( $dbkey, -3 ) == '/..' 03220 ) 03221 ) { 03222 return false; 03223 } 03224 03225 # Magic tilde sequences? Nu-uh! 03226 if ( strpos( $dbkey, '~~~' ) !== false ) { 03227 return false; 03228 } 03229 03230 # Limit the size of titles to 255 bytes. This is typically the size of the 03231 # underlying database field. We make an exception for special pages, which 03232 # don't need to be stored in the database, and may edge over 255 bytes due 03233 # to subpage syntax for long titles, e.g. [[Special:Block/Long name]] 03234 if ( 03235 ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) 03236 || strlen( $dbkey ) > 512 03237 ) { 03238 return false; 03239 } 03240 03241 # Normally, all wiki links are forced to have an initial capital letter so [[foo]] 03242 # and [[Foo]] point to the same place. Don't force it for interwikis, since the 03243 # other site might be case-sensitive. 03244 $this->mUserCaseDBKey = $dbkey; 03245 if ( $this->mInterwiki == '' ) { 03246 $dbkey = self::capitalize( $dbkey, $this->mNamespace ); 03247 } 03248 03249 # Can't make a link to a namespace alone... "empty" local links can only be 03250 # self-links with a fragment identifier. 03251 if ( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) { 03252 return false; 03253 } 03254 03255 // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles. 03256 // IP names are not allowed for accounts, and can only be referring to 03257 // edits from the IP. Given '::' abbreviations and caps/lowercaps, 03258 // there are numerous ways to present the same IP. Having sp:contribs scan 03259 // them all is silly and having some show the edits and others not is 03260 // inconsistent. Same for talk/userpages. Keep them normalized instead. 03261 $dbkey = ( $this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK ) 03262 ? IP::sanitizeIP( $dbkey ) 03263 : $dbkey; 03264 03265 // Any remaining initial :s are illegal. 03266 if ( $dbkey !== '' && ':' == $dbkey[0] ) { 03267 return false; 03268 } 03269 03270 # Fill fields 03271 $this->mDbkeyform = $dbkey; 03272 $this->mUrlform = wfUrlencode( $dbkey ); 03273 03274 $this->mTextform = str_replace( '_', ' ', $dbkey ); 03275 03276 return true; 03277 } 03278 03291 public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03292 if ( count( $options ) > 0 ) { 03293 $db = wfGetDB( DB_MASTER ); 03294 } else { 03295 $db = wfGetDB( DB_SLAVE ); 03296 } 03297 03298 $res = $db->select( 03299 array( 'page', $table ), 03300 self::getSelectFields(), 03301 array( 03302 "{$prefix}_from=page_id", 03303 "{$prefix}_namespace" => $this->getNamespace(), 03304 "{$prefix}_title" => $this->getDBkey() ), 03305 __METHOD__, 03306 $options 03307 ); 03308 03309 $retVal = array(); 03310 if ( $res->numRows() ) { 03311 $linkCache = LinkCache::singleton(); 03312 foreach ( $res as $row ) { 03313 $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ); 03314 if ( $titleObj ) { 03315 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03316 $retVal[] = $titleObj; 03317 } 03318 } 03319 } 03320 return $retVal; 03321 } 03322 03333 public function getTemplateLinksTo( $options = array() ) { 03334 return $this->getLinksTo( $options, 'templatelinks', 'tl' ); 03335 } 03336 03349 public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03350 global $wgContentHandlerUseDB; 03351 03352 $id = $this->getArticleID(); 03353 03354 # If the page doesn't exist; there can't be any link from this page 03355 if ( !$id ) { 03356 return array(); 03357 } 03358 03359 if ( count( $options ) > 0 ) { 03360 $db = wfGetDB( DB_MASTER ); 03361 } else { 03362 $db = wfGetDB( DB_SLAVE ); 03363 } 03364 03365 $namespaceFiled = "{$prefix}_namespace"; 03366 $titleField = "{$prefix}_title"; 03367 03368 $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ); 03369 if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model'; 03370 03371 $res = $db->select( 03372 array( $table, 'page' ), 03373 $fields, 03374 array( "{$prefix}_from" => $id ), 03375 __METHOD__, 03376 $options, 03377 array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) ) 03378 ); 03379 03380 $retVal = array(); 03381 if ( $res->numRows() ) { 03382 $linkCache = LinkCache::singleton(); 03383 foreach ( $res as $row ) { 03384 $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); 03385 if ( $titleObj ) { 03386 if ( $row->page_id ) { 03387 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03388 } else { 03389 $linkCache->addBadLinkObj( $titleObj ); 03390 } 03391 $retVal[] = $titleObj; 03392 } 03393 } 03394 } 03395 return $retVal; 03396 } 03397 03408 public function getTemplateLinksFrom( $options = array() ) { 03409 return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); 03410 } 03411 03418 public function getBrokenLinksFrom() { 03419 if ( $this->getArticleID() == 0 ) { 03420 # All links from article ID 0 are false positives 03421 return array(); 03422 } 03423 03424 $dbr = wfGetDB( DB_SLAVE ); 03425 $res = $dbr->select( 03426 array( 'page', 'pagelinks' ), 03427 array( 'pl_namespace', 'pl_title' ), 03428 array( 03429 'pl_from' => $this->getArticleID(), 03430 'page_namespace IS NULL' 03431 ), 03432 __METHOD__, array(), 03433 array( 03434 'page' => array( 03435 'LEFT JOIN', 03436 array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) 03437 ) 03438 ) 03439 ); 03440 03441 $retVal = array(); 03442 foreach ( $res as $row ) { 03443 $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); 03444 } 03445 return $retVal; 03446 } 03447 03454 public function getSquidURLs() { 03455 $urls = array( 03456 $this->getInternalURL(), 03457 $this->getInternalURL( 'action=history' ) 03458 ); 03459 03460 $pageLang = $this->getPageLanguage(); 03461 if ( $pageLang->hasVariants() ) { 03462 $variants = $pageLang->getVariants(); 03463 foreach ( $variants as $vCode ) { 03464 $urls[] = $this->getInternalURL( '', $vCode ); 03465 } 03466 } 03467 03468 return $urls; 03469 } 03470 03474 public function purgeSquid() { 03475 global $wgUseSquid; 03476 if ( $wgUseSquid ) { 03477 $urls = $this->getSquidURLs(); 03478 $u = new SquidUpdate( $urls ); 03479 $u->doUpdate(); 03480 } 03481 } 03482 03489 public function moveNoAuth( &$nt ) { 03490 return $this->moveTo( $nt, false ); 03491 } 03492 03503 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { 03504 global $wgUser, $wgContentHandlerUseDB; 03505 03506 $errors = array(); 03507 if ( !$nt ) { 03508 // Normally we'd add this to $errors, but we'll get 03509 // lots of syntax errors if $nt is not an object 03510 return array( array( 'badtitletext' ) ); 03511 } 03512 if ( $this->equals( $nt ) ) { 03513 $errors[] = array( 'selfmove' ); 03514 } 03515 if ( !$this->isMovable() ) { 03516 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 03517 } 03518 if ( $nt->getInterwiki() != '' ) { 03519 $errors[] = array( 'immobile-target-namespace-iw' ); 03520 } 03521 if ( !$nt->isMovable() ) { 03522 $errors[] = array( 'immobile-target-namespace', $nt->getNsText() ); 03523 } 03524 03525 $oldid = $this->getArticleID(); 03526 $newid = $nt->getArticleID(); 03527 03528 if ( strlen( $nt->getDBkey() ) < 1 ) { 03529 $errors[] = array( 'articleexists' ); 03530 } 03531 if ( 03532 ( $this->getDBkey() == '' ) || 03533 ( !$oldid ) || 03534 ( $nt->getDBkey() == '' ) 03535 ) { 03536 $errors[] = array( 'badarticleerror' ); 03537 } 03538 03539 // Content model checks 03540 if ( !$wgContentHandlerUseDB && 03541 $this->getContentModel() !== $nt->getContentModel() ) { 03542 // can't move a page if that would change the page's content model 03543 $errors[] = array( 03544 'bad-target-model', 03545 ContentHandler::getLocalizedName( $this->getContentModel() ), 03546 ContentHandler::getLocalizedName( $nt->getContentModel() ) 03547 ); 03548 } 03549 03550 // Image-specific checks 03551 if ( $this->getNamespace() == NS_FILE ) { 03552 $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) ); 03553 } 03554 03555 if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) { 03556 $errors[] = array( 'nonfile-cannot-move-to-file' ); 03557 } 03558 03559 if ( $auth ) { 03560 $errors = wfMergeErrorArrays( $errors, 03561 $this->getUserPermissionsErrors( 'move', $wgUser ), 03562 $this->getUserPermissionsErrors( 'edit', $wgUser ), 03563 $nt->getUserPermissionsErrors( 'move-target', $wgUser ), 03564 $nt->getUserPermissionsErrors( 'edit', $wgUser ) ); 03565 } 03566 03567 $match = EditPage::matchSummarySpamRegex( $reason ); 03568 if ( $match !== false ) { 03569 // This is kind of lame, won't display nice 03570 $errors[] = array( 'spamprotectiontext' ); 03571 } 03572 03573 $err = null; 03574 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) { 03575 $errors[] = array( 'hookaborted', $err ); 03576 } 03577 03578 # The move is allowed only if (1) the target doesn't exist, or 03579 # (2) the target is a redirect to the source, and has no history 03580 # (so we can undo bad moves right after they're done). 03581 03582 if ( 0 != $newid ) { # Target exists; check for validity 03583 if ( !$this->isValidMoveTarget( $nt ) ) { 03584 $errors[] = array( 'articleexists' ); 03585 } 03586 } else { 03587 $tp = $nt->getTitleProtection(); 03588 $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm']; 03589 if ( $tp and !$wgUser->isAllowed( $right ) ) { 03590 $errors[] = array( 'cantmove-titleprotected' ); 03591 } 03592 } 03593 if ( empty( $errors ) ) { 03594 return true; 03595 } 03596 return $errors; 03597 } 03598 03604 protected function validateFileMoveOperation( $nt ) { 03605 global $wgUser; 03606 03607 $errors = array(); 03608 03609 // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below 03610 03611 $file = wfLocalFile( $this ); 03612 if ( $file->exists() ) { 03613 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { 03614 $errors[] = array( 'imageinvalidfilename' ); 03615 } 03616 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) { 03617 $errors[] = array( 'imagetypemismatch' ); 03618 } 03619 } 03620 03621 if ( $nt->getNamespace() != NS_FILE ) { 03622 $errors[] = array( 'imagenocrossnamespace' ); 03623 // From here we want to do checks on a file object, so if we can't 03624 // create one, we must return. 03625 return $errors; 03626 } 03627 03628 // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here 03629 03630 $destFile = wfLocalFile( $nt ); 03631 if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) { 03632 $errors[] = array( 'file-exists-sharedrepo' ); 03633 } 03634 03635 return $errors; 03636 } 03637 03649 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { 03650 global $wgUser; 03651 $err = $this->isValidMoveOperation( $nt, $auth, $reason ); 03652 if ( is_array( $err ) ) { 03653 // Auto-block user's IP if the account was "hard" blocked 03654 $wgUser->spreadAnyEditBlock(); 03655 return $err; 03656 } 03657 // Check suppressredirect permission 03658 if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) { 03659 $createRedirect = true; 03660 } 03661 03662 // If it is a file, move it first. 03663 // It is done before all other moving stuff is done because it's hard to revert. 03664 $dbw = wfGetDB( DB_MASTER ); 03665 if ( $this->getNamespace() == NS_FILE ) { 03666 $file = wfLocalFile( $this ); 03667 if ( $file->exists() ) { 03668 $status = $file->move( $nt ); 03669 if ( !$status->isOk() ) { 03670 return $status->getErrorsArray(); 03671 } 03672 } 03673 // Clear RepoGroup process cache 03674 RepoGroup::singleton()->clearCache( $this ); 03675 RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache 03676 } 03677 03678 $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own. 03679 $pageid = $this->getArticleID( self::GAID_FOR_UPDATE ); 03680 $protected = $this->isProtected(); 03681 03682 // Do the actual move 03683 $this->moveToInternal( $nt, $reason, $createRedirect ); 03684 03685 // Refresh the sortkey for this row. Be careful to avoid resetting 03686 // cl_timestamp, which may disturb time-based lists on some sites. 03687 $prefixes = $dbw->select( 03688 'categorylinks', 03689 array( 'cl_sortkey_prefix', 'cl_to' ), 03690 array( 'cl_from' => $pageid ), 03691 __METHOD__ 03692 ); 03693 foreach ( $prefixes as $prefixRow ) { 03694 $prefix = $prefixRow->cl_sortkey_prefix; 03695 $catTo = $prefixRow->cl_to; 03696 $dbw->update( 'categorylinks', 03697 array( 03698 'cl_sortkey' => Collation::singleton()->getSortKey( 03699 $nt->getCategorySortkey( $prefix ) ), 03700 'cl_timestamp=cl_timestamp' ), 03701 array( 03702 'cl_from' => $pageid, 03703 'cl_to' => $catTo ), 03704 __METHOD__ 03705 ); 03706 } 03707 03708 $redirid = $this->getArticleID(); 03709 03710 if ( $protected ) { 03711 # Protect the redirect title as the title used to be... 03712 $dbw->insertSelect( 'page_restrictions', 'page_restrictions', 03713 array( 03714 'pr_page' => $redirid, 03715 'pr_type' => 'pr_type', 03716 'pr_level' => 'pr_level', 03717 'pr_cascade' => 'pr_cascade', 03718 'pr_user' => 'pr_user', 03719 'pr_expiry' => 'pr_expiry' 03720 ), 03721 array( 'pr_page' => $pageid ), 03722 __METHOD__, 03723 array( 'IGNORE' ) 03724 ); 03725 # Update the protection log 03726 $log = new LogPage( 'protect' ); 03727 $comment = wfMessage( 03728 'prot_1movedto2', 03729 $this->getPrefixedText(), 03730 $nt->getPrefixedText() 03731 )->inContentLanguage()->text(); 03732 if ( $reason ) { 03733 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03734 } 03735 // @todo FIXME: $params? 03736 $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) ); 03737 } 03738 03739 # Update watchlists 03740 $oldnamespace = MWNamespace::getSubject( $this->getNamespace() ); 03741 $newnamespace = MWNamespace::getSubject( $nt->getNamespace() ); 03742 $oldtitle = $this->getDBkey(); 03743 $newtitle = $nt->getDBkey(); 03744 03745 if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) { 03746 WatchedItem::duplicateEntries( $this, $nt ); 03747 } 03748 03749 $dbw->commit( __METHOD__ ); 03750 03751 wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); 03752 return true; 03753 } 03754 03765 private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) { 03766 global $wgUser, $wgContLang; 03767 03768 if ( $nt->exists() ) { 03769 $moveOverRedirect = true; 03770 $logType = 'move_redir'; 03771 } else { 03772 $moveOverRedirect = false; 03773 $logType = 'move'; 03774 } 03775 03776 if ( $createRedirect ) { 03777 $contentHandler = ContentHandler::getForTitle( $this ); 03778 $redirectContent = $contentHandler->makeRedirectContent( $nt ); 03779 03780 // NOTE: If this page's content model does not support redirects, $redirectContent will be null. 03781 } else { 03782 $redirectContent = null; 03783 } 03784 03785 $logEntry = new ManualLogEntry( 'move', $logType ); 03786 $logEntry->setPerformer( $wgUser ); 03787 $logEntry->setTarget( $this ); 03788 $logEntry->setComment( $reason ); 03789 $logEntry->setParameters( array( 03790 '4::target' => $nt->getPrefixedText(), 03791 '5::noredir' => $redirectContent ? '0': '1', 03792 ) ); 03793 03794 $formatter = LogFormatter::newFromEntry( $logEntry ); 03795 $formatter->setContext( RequestContext::newExtraneousContext( $this ) ); 03796 $comment = $formatter->getPlainActionText(); 03797 if ( $reason ) { 03798 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03799 } 03800 # Truncate for whole multibyte characters. 03801 $comment = $wgContLang->truncate( $comment, 255 ); 03802 03803 $oldid = $this->getArticleID(); 03804 03805 $dbw = wfGetDB( DB_MASTER ); 03806 03807 $newpage = WikiPage::factory( $nt ); 03808 03809 if ( $moveOverRedirect ) { 03810 $newid = $nt->getArticleID(); 03811 03812 # Delete the old redirect. We don't save it to history since 03813 # by definition if we've got here it's rather uninteresting. 03814 # We have to remove it so that the next step doesn't trigger 03815 # a conflict on the unique namespace+title index... 03816 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ ); 03817 03818 $newpage->doDeleteUpdates( $newid ); 03819 } 03820 03821 # Save a null revision in the page's history notifying of the move 03822 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); 03823 if ( !is_object( $nullRevision ) ) { 03824 throw new MWException( 'No valid null revision produced in ' . __METHOD__ ); 03825 } 03826 03827 $nullRevision->insertOn( $dbw ); 03828 03829 # Change the name of the target page: 03830 $dbw->update( 'page', 03831 /* SET */ array( 03832 'page_namespace' => $nt->getNamespace(), 03833 'page_title' => $nt->getDBkey(), 03834 ), 03835 /* WHERE */ array( 'page_id' => $oldid ), 03836 __METHOD__ 03837 ); 03838 03839 $this->resetArticleID( 0 ); 03840 $nt->resetArticleID( $oldid ); 03841 03842 $newpage->updateRevisionOn( $dbw, $nullRevision ); 03843 03844 wfRunHooks( 'NewRevisionFromEditComplete', 03845 array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) ); 03846 03847 $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) ); 03848 03849 if ( !$moveOverRedirect ) { 03850 WikiPage::onArticleCreate( $nt ); 03851 } 03852 03853 # Recreate the redirect, this time in the other direction. 03854 if ( !$redirectContent ) { 03855 WikiPage::onArticleDelete( $this ); 03856 } else { 03857 $redirectArticle = WikiPage::factory( $this ); 03858 $newid = $redirectArticle->insertOn( $dbw ); 03859 if ( $newid ) { // sanity 03860 $redirectRevision = new Revision( array( 03861 'title' => $this, // for determining the default content model 03862 'page' => $newid, 03863 'comment' => $comment, 03864 'content' => $redirectContent ) ); 03865 $redirectRevision->insertOn( $dbw ); 03866 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); 03867 03868 wfRunHooks( 'NewRevisionFromEditComplete', 03869 array( $redirectArticle, $redirectRevision, false, $wgUser ) ); 03870 03871 $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) ); 03872 } 03873 } 03874 03875 # Log the move 03876 $logid = $logEntry->insert(); 03877 $logEntry->publish( $logid ); 03878 } 03879 03892 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { 03893 global $wgMaximumMovedPages; 03894 // Check permissions 03895 if ( !$this->userCan( 'move-subpages' ) ) { 03896 return array( 'cant-move-subpages' ); 03897 } 03898 // Do the source and target namespaces support subpages? 03899 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03900 return array( 'namespace-nosubpages', 03901 MWNamespace::getCanonicalName( $this->getNamespace() ) ); 03902 } 03903 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) { 03904 return array( 'namespace-nosubpages', 03905 MWNamespace::getCanonicalName( $nt->getNamespace() ) ); 03906 } 03907 03908 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 ); 03909 $retval = array(); 03910 $count = 0; 03911 foreach ( $subpages as $oldSubpage ) { 03912 $count++; 03913 if ( $count > $wgMaximumMovedPages ) { 03914 $retval[$oldSubpage->getPrefixedTitle()] = 03915 array( 'movepage-max-pages', 03916 $wgMaximumMovedPages ); 03917 break; 03918 } 03919 03920 // We don't know whether this function was called before 03921 // or after moving the root page, so check both 03922 // $this and $nt 03923 if ( $oldSubpage->getArticleID() == $this->getArticleID() || 03924 $oldSubpage->getArticleID() == $nt->getArticleID() ) 03925 { 03926 // When moving a page to a subpage of itself, 03927 // don't move it twice 03928 continue; 03929 } 03930 $newPageName = preg_replace( 03931 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#', 03932 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 03933 $oldSubpage->getDBkey() ); 03934 if ( $oldSubpage->isTalkPage() ) { 03935 $newNs = $nt->getTalkPage()->getNamespace(); 03936 } else { 03937 $newNs = $nt->getSubjectPage()->getNamespace(); 03938 } 03939 # Bug 14385: we need makeTitleSafe because the new page names may 03940 # be longer than 255 characters. 03941 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); 03942 03943 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); 03944 if ( $success === true ) { 03945 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); 03946 } else { 03947 $retval[$oldSubpage->getPrefixedText()] = $success; 03948 } 03949 } 03950 return $retval; 03951 } 03952 03959 public function isSingleRevRedirect() { 03960 global $wgContentHandlerUseDB; 03961 03962 $dbw = wfGetDB( DB_MASTER ); 03963 03964 # Is it a redirect? 03965 $fields = array( 'page_is_redirect', 'page_latest', 'page_id' ); 03966 if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model'; 03967 03968 $row = $dbw->selectRow( 'page', 03969 $fields, 03970 $this->pageCond(), 03971 __METHOD__, 03972 array( 'FOR UPDATE' ) 03973 ); 03974 # Cache some fields we may want 03975 $this->mArticleID = $row ? intval( $row->page_id ) : 0; 03976 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; 03977 $this->mLatestID = $row ? intval( $row->page_latest ) : false; 03978 $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false; 03979 if ( !$this->mRedirect ) { 03980 return false; 03981 } 03982 # Does the article have a history? 03983 $row = $dbw->selectField( array( 'page', 'revision' ), 03984 'rev_id', 03985 array( 'page_namespace' => $this->getNamespace(), 03986 'page_title' => $this->getDBkey(), 03987 'page_id=rev_page', 03988 'page_latest != rev_id' 03989 ), 03990 __METHOD__, 03991 array( 'FOR UPDATE' ) 03992 ); 03993 # Return true if there was no history 03994 return ( $row === false ); 03995 } 03996 04004 public function isValidMoveTarget( $nt ) { 04005 # Is it an existing file? 04006 if ( $nt->getNamespace() == NS_FILE ) { 04007 $file = wfLocalFile( $nt ); 04008 if ( $file->exists() ) { 04009 wfDebug( __METHOD__ . ": file exists\n" ); 04010 return false; 04011 } 04012 } 04013 # Is it a redirect with no history? 04014 if ( !$nt->isSingleRevRedirect() ) { 04015 wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); 04016 return false; 04017 } 04018 # Get the article text 04019 $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST ); 04020 if( !is_object( $rev ) ) { 04021 return false; 04022 } 04023 $content = $rev->getContent(); 04024 # Does the redirect point to the source? 04025 # Or is it a broken self-redirect, usually caused by namespace collisions? 04026 $redirTitle = $content ? $content->getRedirectTarget() : null; 04027 04028 if ( $redirTitle ) { 04029 if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && 04030 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) { 04031 wfDebug( __METHOD__ . ": redirect points to other page\n" ); 04032 return false; 04033 } else { 04034 return true; 04035 } 04036 } else { 04037 # Fail safe (not a redirect after all. strange.) 04038 wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . 04039 " is a redirect, but it doesn't contain a valid redirect.\n" ); 04040 return false; 04041 } 04042 } 04043 04051 public function getParentCategories() { 04052 global $wgContLang; 04053 04054 $data = array(); 04055 04056 $titleKey = $this->getArticleID(); 04057 04058 if ( $titleKey === 0 ) { 04059 return $data; 04060 } 04061 04062 $dbr = wfGetDB( DB_SLAVE ); 04063 04064 $res = $dbr->select( 04065 'categorylinks', 04066 'cl_to', 04067 array( 'cl_from' => $titleKey ), 04068 __METHOD__ 04069 ); 04070 04071 if ( $res->numRows() > 0 ) { 04072 foreach ( $res as $row ) { 04073 // $data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to); 04074 $data[$wgContLang->getNSText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText(); 04075 } 04076 } 04077 return $data; 04078 } 04079 04086 public function getParentCategoryTree( $children = array() ) { 04087 $stack = array(); 04088 $parents = $this->getParentCategories(); 04089 04090 if ( $parents ) { 04091 foreach ( $parents as $parent => $current ) { 04092 if ( array_key_exists( $parent, $children ) ) { 04093 # Circular reference 04094 $stack[$parent] = array(); 04095 } else { 04096 $nt = Title::newFromText( $parent ); 04097 if ( $nt ) { 04098 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) ); 04099 } 04100 } 04101 } 04102 } 04103 04104 return $stack; 04105 } 04106 04113 public function pageCond() { 04114 if ( $this->mArticleID > 0 ) { 04115 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs 04116 return array( 'page_id' => $this->mArticleID ); 04117 } else { 04118 return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); 04119 } 04120 } 04121 04129 public function getPreviousRevisionID( $revId, $flags = 0 ) { 04130 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04131 $revId = $db->selectField( 'revision', 'rev_id', 04132 array( 04133 'rev_page' => $this->getArticleID( $flags ), 04134 'rev_id < ' . intval( $revId ) 04135 ), 04136 __METHOD__, 04137 array( 'ORDER BY' => 'rev_id DESC' ) 04138 ); 04139 04140 if ( $revId === false ) { 04141 return false; 04142 } else { 04143 return intval( $revId ); 04144 } 04145 } 04146 04154 public function getNextRevisionID( $revId, $flags = 0 ) { 04155 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04156 $revId = $db->selectField( 'revision', 'rev_id', 04157 array( 04158 'rev_page' => $this->getArticleID( $flags ), 04159 'rev_id > ' . intval( $revId ) 04160 ), 04161 __METHOD__, 04162 array( 'ORDER BY' => 'rev_id' ) 04163 ); 04164 04165 if ( $revId === false ) { 04166 return false; 04167 } else { 04168 return intval( $revId ); 04169 } 04170 } 04171 04178 public function getFirstRevision( $flags = 0 ) { 04179 $pageId = $this->getArticleID( $flags ); 04180 if ( $pageId ) { 04181 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04182 $row = $db->selectRow( 'revision', Revision::selectFields(), 04183 array( 'rev_page' => $pageId ), 04184 __METHOD__, 04185 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) 04186 ); 04187 if ( $row ) { 04188 return new Revision( $row ); 04189 } 04190 } 04191 return null; 04192 } 04193 04200 public function getEarliestRevTime( $flags = 0 ) { 04201 $rev = $this->getFirstRevision( $flags ); 04202 return $rev ? $rev->getTimestamp() : null; 04203 } 04204 04210 public function isNewPage() { 04211 $dbr = wfGetDB( DB_SLAVE ); 04212 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); 04213 } 04214 04220 public function isBigDeletion() { 04221 global $wgDeleteRevisionsLimit; 04222 04223 if ( !$wgDeleteRevisionsLimit ) { 04224 return false; 04225 } 04226 04227 $revCount = $this->estimateRevisionCount(); 04228 return $revCount > $wgDeleteRevisionsLimit; 04229 } 04230 04236 public function estimateRevisionCount() { 04237 if ( !$this->exists() ) { 04238 return 0; 04239 } 04240 04241 if ( $this->mEstimateRevisions === null ) { 04242 $dbr = wfGetDB( DB_SLAVE ); 04243 $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', 04244 array( 'rev_page' => $this->getArticleID() ), __METHOD__ ); 04245 } 04246 04247 return $this->mEstimateRevisions; 04248 } 04249 04258 public function countRevisionsBetween( $old, $new ) { 04259 if ( !( $old instanceof Revision ) ) { 04260 $old = Revision::newFromTitle( $this, (int)$old ); 04261 } 04262 if ( !( $new instanceof Revision ) ) { 04263 $new = Revision::newFromTitle( $this, (int)$new ); 04264 } 04265 if ( !$old || !$new ) { 04266 return 0; // nothing to compare 04267 } 04268 $dbr = wfGetDB( DB_SLAVE ); 04269 return (int)$dbr->selectField( 'revision', 'count(*)', 04270 array( 04271 'rev_page' => $this->getArticleID(), 04272 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04273 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04274 ), 04275 __METHOD__ 04276 ); 04277 } 04278 04293 public function countAuthorsBetween( $old, $new, $limit, $options = array() ) { 04294 if ( !( $old instanceof Revision ) ) { 04295 $old = Revision::newFromTitle( $this, (int)$old ); 04296 } 04297 if ( !( $new instanceof Revision ) ) { 04298 $new = Revision::newFromTitle( $this, (int)$new ); 04299 } 04300 // XXX: what if Revision objects are passed in, but they don't refer to this title? 04301 // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID() 04302 // in the sanity check below? 04303 if ( !$old || !$new ) { 04304 return 0; // nothing to compare 04305 } 04306 $old_cmp = '>'; 04307 $new_cmp = '<'; 04308 $options = (array)$options; 04309 if ( in_array( 'include_old', $options ) ) { 04310 $old_cmp = '>='; 04311 } 04312 if ( in_array( 'include_new', $options ) ) { 04313 $new_cmp = '<='; 04314 } 04315 if ( in_array( 'include_both', $options ) ) { 04316 $old_cmp = '>='; 04317 $new_cmp = '<='; 04318 } 04319 // No DB query needed if $old and $new are the same or successive revisions: 04320 if ( $old->getId() === $new->getId() ) { 04321 return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; 04322 } else if ( $old->getId() === $new->getParentId() ) { 04323 if ( $old_cmp === '>' || $new_cmp === '<' ) { 04324 return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; 04325 } 04326 return ( $old->getRawUserText() === $new->getRawUserText() ) ? 1 : 2; 04327 } 04328 $dbr = wfGetDB( DB_SLAVE ); 04329 $res = $dbr->select( 'revision', 'DISTINCT rev_user_text', 04330 array( 04331 'rev_page' => $this->getArticleID(), 04332 "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04333 "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04334 ), __METHOD__, 04335 array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated 04336 ); 04337 return (int)$dbr->numRows( $res ); 04338 } 04339 04346 public function equals( Title $title ) { 04347 // Note: === is necessary for proper matching of number-like titles. 04348 return $this->getInterwiki() === $title->getInterwiki() 04349 && $this->getNamespace() == $title->getNamespace() 04350 && $this->getDBkey() === $title->getDBkey(); 04351 } 04352 04359 public function isSubpageOf( Title $title ) { 04360 return $this->getInterwiki() === $title->getInterwiki() 04361 && $this->getNamespace() == $title->getNamespace() 04362 && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; 04363 } 04364 04374 public function exists() { 04375 return $this->getArticleID() != 0; 04376 } 04377 04394 public function isAlwaysKnown() { 04395 $isKnown = null; 04396 04407 wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) ); 04408 04409 if ( !is_null( $isKnown ) ) { 04410 return $isKnown; 04411 } 04412 04413 if ( $this->mInterwiki != '' ) { 04414 return true; // any interwiki link might be viewable, for all we know 04415 } 04416 04417 switch( $this->mNamespace ) { 04418 case NS_MEDIA: 04419 case NS_FILE: 04420 // file exists, possibly in a foreign repo 04421 return (bool)wfFindFile( $this ); 04422 case NS_SPECIAL: 04423 // valid special page 04424 return SpecialPageFactory::exists( $this->getDBkey() ); 04425 case NS_MAIN: 04426 // selflink, possibly with fragment 04427 return $this->mDbkeyform == ''; 04428 case NS_MEDIAWIKI: 04429 // known system message 04430 return $this->hasSourceText() !== false; 04431 default: 04432 return false; 04433 } 04434 } 04435 04447 public function isKnown() { 04448 return $this->isAlwaysKnown() || $this->exists(); 04449 } 04450 04456 public function hasSourceText() { 04457 if ( $this->exists() ) { 04458 return true; 04459 } 04460 04461 if ( $this->mNamespace == NS_MEDIAWIKI ) { 04462 // If the page doesn't exist but is a known system message, default 04463 // message content will be displayed, same for language subpages- 04464 // Use always content language to avoid loading hundreds of languages 04465 // to get the link color. 04466 global $wgContLang; 04467 list( $name, ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04468 $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false ); 04469 return $message->exists(); 04470 } 04471 04472 return false; 04473 } 04474 04480 public function getDefaultMessageText() { 04481 global $wgContLang; 04482 04483 if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case 04484 return false; 04485 } 04486 04487 list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04488 $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false ); 04489 04490 if ( $message->exists() ) { 04491 return $message->plain(); 04492 } else { 04493 return false; 04494 } 04495 } 04496 04502 public function invalidateCache() { 04503 global $wgMemc; 04504 if ( wfReadOnly() ) { 04505 return false; 04506 } 04507 $dbw = wfGetDB( DB_MASTER ); 04508 $success = $dbw->update( 04509 'page', 04510 array( 'page_touched' => $dbw->timestamp() ), 04511 $this->pageCond(), 04512 __METHOD__ 04513 ); 04514 HTMLFileCache::clearFileCache( $this ); 04515 04516 // Clear page info. 04517 $revision = WikiPage::factory( $this )->getRevision(); 04518 if( $revision !== null ) { 04519 $memcKey = wfMemcKey( 'infoaction', $this->getPrefixedText(), $revision->getId() ); 04520 $success = $success && $wgMemc->delete( $memcKey ); 04521 } 04522 04523 return $success; 04524 } 04525 04531 public function touchLinks() { 04532 $u = new HTMLCacheUpdate( $this, 'pagelinks' ); 04533 $u->doUpdate(); 04534 04535 if ( $this->getNamespace() == NS_CATEGORY ) { 04536 $u = new HTMLCacheUpdate( $this, 'categorylinks' ); 04537 $u->doUpdate(); 04538 } 04539 } 04540 04547 public function getTouched( $db = null ) { 04548 $db = isset( $db ) ? $db : wfGetDB( DB_SLAVE ); 04549 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); 04550 return $touched; 04551 } 04552 04559 public function getNotificationTimestamp( $user = null ) { 04560 global $wgUser, $wgShowUpdatedMarker; 04561 // Assume current user if none given 04562 if ( !$user ) { 04563 $user = $wgUser; 04564 } 04565 // Check cache first 04566 $uid = $user->getId(); 04567 // avoid isset here, as it'll return false for null entries 04568 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) { 04569 return $this->mNotificationTimestamp[$uid]; 04570 } 04571 if ( !$uid || !$wgShowUpdatedMarker ) { 04572 return $this->mNotificationTimestamp[$uid] = false; 04573 } 04574 // Don't cache too much! 04575 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) { 04576 $this->mNotificationTimestamp = array(); 04577 } 04578 $dbr = wfGetDB( DB_SLAVE ); 04579 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', 04580 'wl_notificationtimestamp', 04581 array( 04582 'wl_user' => $user->getId(), 04583 'wl_namespace' => $this->getNamespace(), 04584 'wl_title' => $this->getDBkey(), 04585 ), 04586 __METHOD__ 04587 ); 04588 return $this->mNotificationTimestamp[$uid]; 04589 } 04590 04597 public function getNamespaceKey( $prepend = 'nstab-' ) { 04598 global $wgContLang; 04599 // Gets the subject namespace if this title 04600 $namespace = MWNamespace::getSubject( $this->getNamespace() ); 04601 // Checks if canonical namespace name exists for namespace 04602 if ( MWNamespace::exists( $this->getNamespace() ) ) { 04603 // Uses canonical namespace name 04604 $namespaceKey = MWNamespace::getCanonicalName( $namespace ); 04605 } else { 04606 // Uses text of namespace 04607 $namespaceKey = $this->getSubjectNsText(); 04608 } 04609 // Makes namespace key lowercase 04610 $namespaceKey = $wgContLang->lc( $namespaceKey ); 04611 // Uses main 04612 if ( $namespaceKey == '' ) { 04613 $namespaceKey = 'main'; 04614 } 04615 // Changes file to image for backwards compatibility 04616 if ( $namespaceKey == 'file' ) { 04617 $namespaceKey = 'image'; 04618 } 04619 return $prepend . $namespaceKey; 04620 } 04621 04628 public function getRedirectsHere( $ns = null ) { 04629 $redirs = array(); 04630 04631 $dbr = wfGetDB( DB_SLAVE ); 04632 $where = array( 04633 'rd_namespace' => $this->getNamespace(), 04634 'rd_title' => $this->getDBkey(), 04635 'rd_from = page_id' 04636 ); 04637 if ( $this->isExternal() ) { 04638 $where['rd_interwiki'] = $this->getInterwiki(); 04639 } else { 04640 $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'; 04641 } 04642 if ( !is_null( $ns ) ) { 04643 $where['page_namespace'] = $ns; 04644 } 04645 04646 $res = $dbr->select( 04647 array( 'redirect', 'page' ), 04648 array( 'page_namespace', 'page_title' ), 04649 $where, 04650 __METHOD__ 04651 ); 04652 04653 foreach ( $res as $row ) { 04654 $redirs[] = self::newFromRow( $row ); 04655 } 04656 return $redirs; 04657 } 04658 04664 public function isValidRedirectTarget() { 04665 global $wgInvalidRedirectTargets; 04666 04667 // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here 04668 if ( $this->isSpecial( 'Userlogout' ) ) { 04669 return false; 04670 } 04671 04672 foreach ( $wgInvalidRedirectTargets as $target ) { 04673 if ( $this->isSpecial( $target ) ) { 04674 return false; 04675 } 04676 } 04677 04678 return true; 04679 } 04680 04686 public function getBacklinkCache() { 04687 return BacklinkCache::get( $this ); 04688 } 04689 04695 public function canUseNoindex() { 04696 global $wgContentNamespaces, $wgExemptFromUserRobotsControl; 04697 04698 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl ) 04699 ? $wgContentNamespaces 04700 : $wgExemptFromUserRobotsControl; 04701 04702 return !in_array( $this->mNamespace, $bannedNamespaces ); 04703 04704 } 04705 04716 public function getCategorySortkey( $prefix = '' ) { 04717 $unprefixed = $this->getText(); 04718 04719 // Anything that uses this hook should only depend 04720 // on the Title object passed in, and should probably 04721 // tell the users to run updateCollations.php --force 04722 // in order to re-sort existing category relations. 04723 wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) ); 04724 if ( $prefix !== '' ) { 04725 # Separate with a line feed, so the unprefixed part is only used as 04726 # a tiebreaker when two pages have the exact same prefix. 04727 # In UCA, tab is the only character that can sort above LF 04728 # so we strip both of them from the original prefix. 04729 $prefix = strtr( $prefix, "\n\t", ' ' ); 04730 return "$prefix\n$unprefixed"; 04731 } 04732 return $unprefixed; 04733 } 04734 04743 public function getPageLanguage() { 04744 global $wgLang; 04745 if ( $this->isSpecialPage() ) { 04746 // special pages are in the user language 04747 return $wgLang; 04748 } 04749 04750 //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request. 04751 //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language! 04752 $contentHandler = ContentHandler::getForTitle( $this ); 04753 $pageLang = $contentHandler->getPageLanguage( $this ); 04754 04755 return wfGetLangObj( $pageLang ); 04756 } 04757 04766 public function getPageViewLanguage() { 04767 global $wgLang; 04768 04769 if ( $this->isSpecialPage() ) { 04770 // If the user chooses a variant, the content is actually 04771 // in a language whose code is the variant code. 04772 $variant = $wgLang->getPreferredVariant(); 04773 if ( $wgLang->getCode() !== $variant ) { 04774 return Language::factory( $variant ); 04775 } 04776 04777 return $wgLang; 04778 } 04779 04780 //NOTE: can't be cached persistently, depends on user settings 04781 //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language! 04782 $contentHandler = ContentHandler::getForTitle( $this ); 04783 $pageLang = $contentHandler->getPageViewLanguage( $this ); 04784 return $pageLang; 04785 } 04786 04796 public function getEditNotices() { 04797 $notices = array(); 04798 04799 # Optional notices on a per-namespace and per-page basis 04800 $editnotice_ns = 'editnotice-' . $this->getNamespace(); 04801 $editnotice_ns_message = wfMessage( $editnotice_ns ); 04802 if ( $editnotice_ns_message->exists() ) { 04803 $notices[$editnotice_ns] = $editnotice_ns_message->parseAsBlock(); 04804 } 04805 if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) { 04806 $parts = explode( '/', $this->getDBkey() ); 04807 $editnotice_base = $editnotice_ns; 04808 while ( count( $parts ) > 0 ) { 04809 $editnotice_base .= '-' . array_shift( $parts ); 04810 $editnotice_base_msg = wfMessage( $editnotice_base ); 04811 if ( $editnotice_base_msg->exists() ) { 04812 $notices[$editnotice_base] = $editnotice_base_msg->parseAsBlock(); 04813 } 04814 } 04815 } else { 04816 # Even if there are no subpages in namespace, we still don't want / in MW ns. 04817 $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() ); 04818 $editnoticeMsg = wfMessage( $editnoticeText ); 04819 if ( $editnoticeMsg->exists() ) { 04820 $notices[$editnoticeText] = $editnoticeMsg->parseAsBlock(); 04821 } 04822 } 04823 return $notices; 04824 } 04825 }