MediaWiki
REL1_22
|
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 } 00295 if ( isset( $row->page_len ) ) { 00296 $this->mLength = (int)$row->page_len; 00297 } 00298 if ( isset( $row->page_is_redirect ) ) { 00299 $this->mRedirect = (bool)$row->page_is_redirect; 00300 } 00301 if ( isset( $row->page_latest ) ) { 00302 $this->mLatestID = (int)$row->page_latest; 00303 } 00304 if ( isset( $row->page_content_model ) ) { 00305 $this->mContentModel = strval( $row->page_content_model ); 00306 } else { 00307 $this->mContentModel = false; # initialized lazily in getContentModel() 00308 } 00309 } else { // page not found 00310 $this->mArticleID = 0; 00311 $this->mLength = 0; 00312 $this->mRedirect = false; 00313 $this->mLatestID = 0; 00314 $this->mContentModel = false; # initialized lazily in getContentModel() 00315 } 00316 } 00317 00331 public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { 00332 $t = new Title(); 00333 $t->mInterwiki = $interwiki; 00334 $t->mFragment = $fragment; 00335 $t->mNamespace = $ns = intval( $ns ); 00336 $t->mDbkeyform = str_replace( ' ', '_', $title ); 00337 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; 00338 $t->mUrlform = wfUrlencode( $t->mDbkeyform ); 00339 $t->mTextform = str_replace( '_', ' ', $title ); 00340 $t->mContentModel = false; # initialized lazily in getContentModel() 00341 return $t; 00342 } 00343 00355 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { 00356 if ( !MWNamespace::exists( $ns ) ) { 00357 return null; 00358 } 00359 00360 $t = new Title(); 00361 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki ); 00362 if ( $t->secureAndSplit() ) { 00363 return $t; 00364 } else { 00365 return null; 00366 } 00367 } 00368 00374 public static function newMainPage() { 00375 $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() ); 00376 // Don't give fatal errors if the message is broken 00377 if ( !$title ) { 00378 $title = Title::newFromText( 'Main Page' ); 00379 } 00380 return $title; 00381 } 00382 00393 public static function newFromRedirect( $text ) { 00394 ContentHandler::deprecated( __METHOD__, '1.21' ); 00395 00396 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00397 return $content->getRedirectTarget(); 00398 } 00399 00410 public static function newFromRedirectRecurse( $text ) { 00411 ContentHandler::deprecated( __METHOD__, '1.21' ); 00412 00413 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00414 return $content->getUltimateRedirectTarget(); 00415 } 00416 00427 public static function newFromRedirectArray( $text ) { 00428 ContentHandler::deprecated( __METHOD__, '1.21' ); 00429 00430 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00431 return $content->getRedirectChain(); 00432 } 00433 00440 public static function nameOf( $id ) { 00441 $dbr = wfGetDB( DB_SLAVE ); 00442 00443 $s = $dbr->selectRow( 00444 'page', 00445 array( 'page_namespace', 'page_title' ), 00446 array( 'page_id' => $id ), 00447 __METHOD__ 00448 ); 00449 if ( $s === false ) { 00450 return null; 00451 } 00452 00453 $n = self::makeName( $s->page_namespace, $s->page_title ); 00454 return $n; 00455 } 00456 00462 public static function legalChars() { 00463 global $wgLegalTitleChars; 00464 return $wgLegalTitleChars; 00465 } 00466 00474 static function getTitleInvalidRegex() { 00475 static $rxTc = false; 00476 if ( !$rxTc ) { 00477 # Matching titles will be held as illegal. 00478 $rxTc = '/' . 00479 # Any character not allowed is forbidden... 00480 '[^' . self::legalChars() . ']' . 00481 # URL percent encoding sequences interfere with the ability 00482 # to round-trip titles -- you can't link to them consistently. 00483 '|%[0-9A-Fa-f]{2}' . 00484 # XML/HTML character references produce similar issues. 00485 '|&[A-Za-z0-9\x80-\xff]+;' . 00486 '|&#[0-9]+;' . 00487 '|&#x[0-9A-Fa-f]+;' . 00488 '/S'; 00489 } 00490 00491 return $rxTc; 00492 } 00493 00503 public static function convertByteClassToUnicodeClass( $byteClass ) { 00504 $length = strlen( $byteClass ); 00505 // Input token queue 00506 $x0 = $x1 = $x2 = ''; 00507 // Decoded queue 00508 $d0 = $d1 = $d2 = ''; 00509 // Decoded integer codepoints 00510 $ord0 = $ord1 = $ord2 = 0; 00511 // Re-encoded queue 00512 $r0 = $r1 = $r2 = ''; 00513 // Output 00514 $out = ''; 00515 // Flags 00516 $allowUnicode = false; 00517 for ( $pos = 0; $pos < $length; $pos++ ) { 00518 // Shift the queues down 00519 $x2 = $x1; 00520 $x1 = $x0; 00521 $d2 = $d1; 00522 $d1 = $d0; 00523 $ord2 = $ord1; 00524 $ord1 = $ord0; 00525 $r2 = $r1; 00526 $r1 = $r0; 00527 // Load the current input token and decoded values 00528 $inChar = $byteClass[$pos]; 00529 if ( $inChar == '\\' ) { 00530 if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) { 00531 $x0 = $inChar . $m[0]; 00532 $d0 = chr( hexdec( $m[1] ) ); 00533 $pos += strlen( $m[0] ); 00534 } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) { 00535 $x0 = $inChar . $m[0]; 00536 $d0 = chr( octdec( $m[0] ) ); 00537 $pos += strlen( $m[0] ); 00538 } elseif ( $pos + 1 >= $length ) { 00539 $x0 = $d0 = '\\'; 00540 } else { 00541 $d0 = $byteClass[$pos + 1]; 00542 $x0 = $inChar . $d0; 00543 $pos += 1; 00544 } 00545 } else { 00546 $x0 = $d0 = $inChar; 00547 } 00548 $ord0 = ord( $d0 ); 00549 // Load the current re-encoded value 00550 if ( $ord0 < 32 || $ord0 == 0x7f ) { 00551 $r0 = sprintf( '\x%02x', $ord0 ); 00552 } elseif ( $ord0 >= 0x80 ) { 00553 // Allow unicode if a single high-bit character appears 00554 $r0 = sprintf( '\x%02x', $ord0 ); 00555 $allowUnicode = true; 00556 } elseif ( strpos( '-\\[]^', $d0 ) !== false ) { 00557 $r0 = '\\' . $d0; 00558 } else { 00559 $r0 = $d0; 00560 } 00561 // Do the output 00562 if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) { 00563 // Range 00564 if ( $ord2 > $ord0 ) { 00565 // Empty range 00566 } elseif ( $ord0 >= 0x80 ) { 00567 // Unicode range 00568 $allowUnicode = true; 00569 if ( $ord2 < 0x80 ) { 00570 // Keep the non-unicode section of the range 00571 $out .= "$r2-\\x7F"; 00572 } 00573 } else { 00574 // Normal range 00575 $out .= "$r2-$r0"; 00576 } 00577 // Reset state to the initial value 00578 $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = ''; 00579 } elseif ( $ord2 < 0x80 ) { 00580 // ASCII character 00581 $out .= $r2; 00582 } 00583 } 00584 if ( $ord1 < 0x80 ) { 00585 $out .= $r1; 00586 } 00587 if ( $ord0 < 0x80 ) { 00588 $out .= $r0; 00589 } 00590 if ( $allowUnicode ) { 00591 $out .= '\u0080-\uFFFF'; 00592 } 00593 return $out; 00594 } 00595 00604 public static function indexTitle( $ns, $title ) { 00605 global $wgContLang; 00606 00607 $lc = SearchEngine::legalSearchChars() . '&#;'; 00608 $t = $wgContLang->normalizeForSearch( $title ); 00609 $t = preg_replace( "/[^{$lc}]+/", ' ', $t ); 00610 $t = $wgContLang->lc( $t ); 00611 00612 # Handle 's, s' 00613 $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t ); 00614 $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t ); 00615 00616 $t = preg_replace( "/\\s+/", ' ', $t ); 00617 00618 if ( $ns == NS_FILE ) { 00619 $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t ); 00620 } 00621 return trim( $t ); 00622 } 00623 00633 public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) { 00634 global $wgContLang; 00635 00636 $namespace = $wgContLang->getNsText( $ns ); 00637 $name = $namespace == '' ? $title : "$namespace:$title"; 00638 if ( strval( $interwiki ) != '' ) { 00639 $name = "$interwiki:$name"; 00640 } 00641 if ( strval( $fragment ) != '' ) { 00642 $name .= '#' . $fragment; 00643 } 00644 return $name; 00645 } 00646 00653 static function escapeFragmentForURL( $fragment ) { 00654 # Note that we don't urlencode the fragment. urlencoded Unicode 00655 # fragments appear not to work in IE (at least up to 7) or in at least 00656 # one version of Opera 9.x. The W3C validator, for one, doesn't seem 00657 # to care if they aren't encoded. 00658 return Sanitizer::escapeId( $fragment, 'noninitial' ); 00659 } 00660 00669 public static function compare( $a, $b ) { 00670 if ( $a->getNamespace() == $b->getNamespace() ) { 00671 return strcmp( $a->getText(), $b->getText() ); 00672 } else { 00673 return $a->getNamespace() - $b->getNamespace(); 00674 } 00675 } 00676 00683 public function isLocal() { 00684 if ( $this->mInterwiki != '' ) { 00685 $iw = Interwiki::fetch( $this->mInterwiki ); 00686 if ( $iw ) { 00687 return $iw->isLocal(); 00688 } 00689 } 00690 return true; 00691 } 00692 00698 public function isExternal() { 00699 return ( $this->mInterwiki != '' ); 00700 } 00701 00707 public function getInterwiki() { 00708 return $this->mInterwiki; 00709 } 00710 00717 public function isTrans() { 00718 if ( $this->mInterwiki == '' ) { 00719 return false; 00720 } 00721 00722 return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); 00723 } 00724 00730 public function getTransWikiID() { 00731 if ( $this->mInterwiki == '' ) { 00732 return false; 00733 } 00734 00735 return Interwiki::fetch( $this->mInterwiki )->getWikiID(); 00736 } 00737 00743 public function getText() { 00744 return $this->mTextform; 00745 } 00746 00752 public function getPartialURL() { 00753 return $this->mUrlform; 00754 } 00755 00761 public function getDBkey() { 00762 return $this->mDbkeyform; 00763 } 00764 00770 function getUserCaseDBKey() { 00771 return $this->mUserCaseDBKey; 00772 } 00773 00779 public function getNamespace() { 00780 return $this->mNamespace; 00781 } 00782 00789 public function getContentModel() { 00790 if ( !$this->mContentModel ) { 00791 $linkCache = LinkCache::singleton(); 00792 $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); 00793 } 00794 00795 if ( !$this->mContentModel ) { 00796 $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); 00797 } 00798 00799 if ( !$this->mContentModel ) { 00800 throw new MWException( 'Failed to determine content model!' ); 00801 } 00802 00803 return $this->mContentModel; 00804 } 00805 00812 public function hasContentModel( $id ) { 00813 return $this->getContentModel() == $id; 00814 } 00815 00821 public function getNsText() { 00822 global $wgContLang; 00823 00824 if ( $this->mInterwiki != '' ) { 00825 // This probably shouldn't even happen. ohh man, oh yuck. 00826 // But for interwiki transclusion it sometimes does. 00827 // Shit. Shit shit shit. 00828 // 00829 // Use the canonical namespaces if possible to try to 00830 // resolve a foreign namespace. 00831 if ( MWNamespace::exists( $this->mNamespace ) ) { 00832 return MWNamespace::getCanonicalName( $this->mNamespace ); 00833 } 00834 } 00835 00836 if ( $wgContLang->needsGenderDistinction() && 00837 MWNamespace::hasGenderDistinction( $this->mNamespace ) ) { 00838 $gender = GenderCache::singleton()->getGenderOf( $this->getText(), __METHOD__ ); 00839 return $wgContLang->getGenderNsText( $this->mNamespace, $gender ); 00840 } 00841 00842 return $wgContLang->getNsText( $this->mNamespace ); 00843 } 00844 00850 public function getSubjectNsText() { 00851 global $wgContLang; 00852 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) ); 00853 } 00854 00860 public function getTalkNsText() { 00861 global $wgContLang; 00862 return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ); 00863 } 00864 00870 public function canTalk() { 00871 return MWNamespace::canTalk( $this->mNamespace ); 00872 } 00873 00880 public function canExist() { 00881 return $this->mNamespace >= NS_MAIN; 00882 } 00883 00889 public function isWatchable() { 00890 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); 00891 } 00892 00898 public function isSpecialPage() { 00899 return $this->getNamespace() == NS_SPECIAL; 00900 } 00901 00908 public function isSpecial( $name ) { 00909 if ( $this->isSpecialPage() ) { 00910 list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); 00911 if ( $name == $thisName ) { 00912 return true; 00913 } 00914 } 00915 return false; 00916 } 00917 00924 public function fixSpecialName() { 00925 if ( $this->isSpecialPage() ) { 00926 list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); 00927 if ( $canonicalName ) { 00928 $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); 00929 if ( $localName != $this->mDbkeyform ) { 00930 return Title::makeTitle( NS_SPECIAL, $localName ); 00931 } 00932 } 00933 } 00934 return $this; 00935 } 00936 00947 public function inNamespace( $ns ) { 00948 return MWNamespace::equals( $this->getNamespace(), $ns ); 00949 } 00950 00958 public function inNamespaces( /* ... */ ) { 00959 $namespaces = func_get_args(); 00960 if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { 00961 $namespaces = $namespaces[0]; 00962 } 00963 00964 foreach ( $namespaces as $ns ) { 00965 if ( $this->inNamespace( $ns ) ) { 00966 return true; 00967 } 00968 } 00969 00970 return false; 00971 } 00972 00986 public function hasSubjectNamespace( $ns ) { 00987 return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); 00988 } 00989 00997 public function isContentPage() { 00998 return MWNamespace::isContent( $this->getNamespace() ); 00999 } 01000 01007 public function isMovable() { 01008 if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) { 01009 // Interwiki title or immovable namespace. Hooks don't get to override here 01010 return false; 01011 } 01012 01013 $result = true; 01014 wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); 01015 return $result; 01016 } 01017 01028 public function isMainPage() { 01029 return $this->equals( Title::newMainPage() ); 01030 } 01031 01037 public function isSubpage() { 01038 return MWNamespace::hasSubpages( $this->mNamespace ) 01039 ? strpos( $this->getText(), '/' ) !== false 01040 : false; 01041 } 01042 01048 public function isConversionTable() { 01049 // @todo ConversionTable should become a separate content model. 01050 01051 return $this->getNamespace() == NS_MEDIAWIKI && 01052 strpos( $this->getText(), 'Conversiontable/' ) === 0; 01053 } 01054 01060 public function isWikitextPage() { 01061 return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); 01062 } 01063 01075 public function isCssOrJsPage() { 01076 $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace 01077 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 01078 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01079 01080 #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure 01081 # hook functions can force this method to return true even outside the mediawiki namespace. 01082 01083 wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) ); 01084 01085 return $isCssOrJsPage; 01086 } 01087 01092 public function isCssJsSubpage() { 01093 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01094 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 01095 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); 01096 } 01097 01103 public function getSkinFromCssJsSubpage() { 01104 $subpage = explode( '/', $this->mTextform ); 01105 $subpage = $subpage[count( $subpage ) - 1]; 01106 $lastdot = strrpos( $subpage, '.' ); 01107 if ( $lastdot === false ) { 01108 return $subpage; # Never happens: only called for names ending in '.css' or '.js' 01109 } 01110 return substr( $subpage, 0, $lastdot ); 01111 } 01112 01118 public function isCssSubpage() { 01119 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01120 && $this->hasContentModel( CONTENT_MODEL_CSS ) ); 01121 } 01122 01128 public function isJsSubpage() { 01129 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01130 && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01131 } 01132 01138 public function isTalkPage() { 01139 return MWNamespace::isTalk( $this->getNamespace() ); 01140 } 01141 01147 public function getTalkPage() { 01148 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); 01149 } 01150 01157 public function getSubjectPage() { 01158 // Is this the same title? 01159 $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); 01160 if ( $this->getNamespace() == $subjectNS ) { 01161 return $this; 01162 } 01163 return Title::makeTitle( $subjectNS, $this->getDBkey() ); 01164 } 01165 01171 public function getDefaultNamespace() { 01172 return $this->mDefaultNamespace; 01173 } 01174 01181 public function getIndexTitle() { 01182 return Title::indexTitle( $this->mNamespace, $this->mTextform ); 01183 } 01184 01190 public function getFragment() { 01191 return $this->mFragment; 01192 } 01193 01198 public function getFragmentForURL() { 01199 if ( $this->mFragment == '' ) { 01200 return ''; 01201 } else { 01202 return '#' . Title::escapeFragmentForURL( $this->mFragment ); 01203 } 01204 } 01205 01216 public function setFragment( $fragment ) { 01217 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); 01218 } 01219 01228 private function prefix( $name ) { 01229 $p = ''; 01230 if ( $this->mInterwiki != '' ) { 01231 $p = $this->mInterwiki . ':'; 01232 } 01233 01234 if ( 0 != $this->mNamespace ) { 01235 $p .= $this->getNsText() . ':'; 01236 } 01237 return $p . $name; 01238 } 01239 01246 public function getPrefixedDBkey() { 01247 $s = $this->prefix( $this->mDbkeyform ); 01248 $s = str_replace( ' ', '_', $s ); 01249 return $s; 01250 } 01251 01258 public function getPrefixedText() { 01259 // @todo FIXME: Bad usage of empty() ? 01260 if ( empty( $this->mPrefixedText ) ) { 01261 $s = $this->prefix( $this->mTextform ); 01262 $s = str_replace( '_', ' ', $s ); 01263 $this->mPrefixedText = $s; 01264 } 01265 return $this->mPrefixedText; 01266 } 01267 01273 public function __toString() { 01274 return $this->getPrefixedText(); 01275 } 01276 01283 public function getFullText() { 01284 $text = $this->getPrefixedText(); 01285 if ( $this->mFragment != '' ) { 01286 $text .= '#' . $this->mFragment; 01287 } 01288 return $text; 01289 } 01290 01303 public function getRootText() { 01304 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01305 return $this->getText(); 01306 } 01307 01308 return strtok( $this->getText(), '/' ); 01309 } 01310 01323 public function getRootTitle() { 01324 return Title::makeTitle( $this->getNamespace(), $this->getRootText() ); 01325 } 01326 01338 public function getBaseText() { 01339 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01340 return $this->getText(); 01341 } 01342 01343 $parts = explode( '/', $this->getText() ); 01344 # Don't discard the real title if there's no subpage involved 01345 if ( count( $parts ) > 1 ) { 01346 unset( $parts[count( $parts ) - 1] ); 01347 } 01348 return implode( '/', $parts ); 01349 } 01350 01363 public function getBaseTitle() { 01364 return Title::makeTitle( $this->getNamespace(), $this->getBaseText() ); 01365 } 01366 01378 public function getSubpageText() { 01379 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01380 return $this->mTextform; 01381 } 01382 $parts = explode( '/', $this->mTextform ); 01383 return $parts[count( $parts ) - 1]; 01384 } 01385 01399 public function getSubpage( $text ) { 01400 return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text ); 01401 } 01402 01410 public function getEscapedText() { 01411 wfDeprecated( __METHOD__, '1.19' ); 01412 return htmlspecialchars( $this->getPrefixedText() ); 01413 } 01414 01420 public function getSubpageUrlForm() { 01421 $text = $this->getSubpageText(); 01422 $text = wfUrlencode( str_replace( ' ', '_', $text ) ); 01423 return $text; 01424 } 01425 01431 public function getPrefixedURL() { 01432 $s = $this->prefix( $this->mDbkeyform ); 01433 $s = wfUrlencode( str_replace( ' ', '_', $s ) ); 01434 return $s; 01435 } 01436 01450 private static function fixUrlQueryArgs( $query, $query2 = false ) { 01451 if ( $query2 !== false ) { 01452 wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " . 01453 "method called with a second parameter is deprecated. Add your " . 01454 "parameter to an array passed as the first parameter.", "1.19" ); 01455 } 01456 if ( is_array( $query ) ) { 01457 $query = wfArrayToCgi( $query ); 01458 } 01459 if ( $query2 ) { 01460 if ( is_string( $query2 ) ) { 01461 // $query2 is a string, we will consider this to be 01462 // a deprecated $variant argument and add it to the query 01463 $query2 = wfArrayToCgi( array( 'variant' => $query2 ) ); 01464 } else { 01465 $query2 = wfArrayToCgi( $query2 ); 01466 } 01467 // If we have $query content add a & to it first 01468 if ( $query ) { 01469 $query .= '&'; 01470 } 01471 // Now append the queries together 01472 $query .= $query2; 01473 } 01474 return $query; 01475 } 01476 01490 public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01491 $query = self::fixUrlQueryArgs( $query, $query2 ); 01492 01493 # Hand off all the decisions on urls to getLocalURL 01494 $url = $this->getLocalURL( $query ); 01495 01496 # Expand the url to make it a full url. Note that getLocalURL has the 01497 # potential to output full urls for a variety of reasons, so we use 01498 # wfExpandUrl instead of simply prepending $wgServer 01499 $url = wfExpandUrl( $url, $proto ); 01500 01501 # Finally, add the fragment. 01502 $url .= $this->getFragmentForURL(); 01503 01504 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); 01505 return $url; 01506 } 01507 01525 public function getLocalURL( $query = '', $query2 = false ) { 01526 global $wgArticlePath, $wgScript, $wgServer, $wgRequest; 01527 01528 $query = self::fixUrlQueryArgs( $query, $query2 ); 01529 01530 $interwiki = Interwiki::fetch( $this->mInterwiki ); 01531 if ( $interwiki ) { 01532 $namespace = $this->getNsText(); 01533 if ( $namespace != '' ) { 01534 # Can this actually happen? Interwikis shouldn't be parsed. 01535 # Yes! It can in interwiki transclusion. But... it probably shouldn't. 01536 $namespace .= ':'; 01537 } 01538 $url = $interwiki->getURL( $namespace . $this->getDBkey() ); 01539 $url = wfAppendQuery( $url, $query ); 01540 } else { 01541 $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); 01542 if ( $query == '' ) { 01543 $url = str_replace( '$1', $dbkey, $wgArticlePath ); 01544 wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); 01545 } else { 01546 global $wgVariantArticlePath, $wgActionPaths, $wgContLang; 01547 $url = false; 01548 $matches = array(); 01549 01550 if ( !empty( $wgActionPaths ) && 01551 preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) 01552 { 01553 $action = urldecode( $matches[2] ); 01554 if ( isset( $wgActionPaths[$action] ) ) { 01555 $query = $matches[1]; 01556 if ( isset( $matches[4] ) ) { 01557 $query .= $matches[4]; 01558 } 01559 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); 01560 if ( $query != '' ) { 01561 $url = wfAppendQuery( $url, $query ); 01562 } 01563 } 01564 } 01565 01566 if ( $url === false && 01567 $wgVariantArticlePath && 01568 $wgContLang->getCode() === $this->getPageLanguage()->getCode() && 01569 $this->getPageLanguage()->hasVariants() && 01570 preg_match( '/^variant=([^&]*)$/', $query, $matches ) ) 01571 { 01572 $variant = urldecode( $matches[1] ); 01573 if ( $this->getPageLanguage()->hasVariant( $variant ) ) { 01574 // Only do the variant replacement if the given variant is a valid 01575 // variant for the page's language. 01576 $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); 01577 $url = str_replace( '$1', $dbkey, $url ); 01578 } 01579 } 01580 01581 if ( $url === false ) { 01582 if ( $query == '-' ) { 01583 $query = ''; 01584 } 01585 $url = "{$wgScript}?title={$dbkey}&{$query}"; 01586 } 01587 } 01588 01589 wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) ); 01590 01591 // @todo FIXME: This causes breakage in various places when we 01592 // actually expected a local URL and end up with dupe prefixes. 01593 if ( $wgRequest->getVal( 'action' ) == 'render' ) { 01594 $url = $wgServer . $url; 01595 } 01596 } 01597 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) ); 01598 return $url; 01599 } 01600 01619 public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01620 wfProfileIn( __METHOD__ ); 01621 if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) { 01622 $ret = $this->getFullURL( $query, $query2, $proto ); 01623 } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { 01624 $ret = $this->getFragmentForURL(); 01625 } else { 01626 $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL(); 01627 } 01628 wfProfileOut( __METHOD__ ); 01629 return $ret; 01630 } 01631 01644 public function escapeLocalURL( $query = '', $query2 = false ) { 01645 wfDeprecated( __METHOD__, '1.19' ); 01646 return htmlspecialchars( $this->getLocalURL( $query, $query2 ) ); 01647 } 01648 01659 public function escapeFullURL( $query = '', $query2 = false ) { 01660 wfDeprecated( __METHOD__, '1.19' ); 01661 return htmlspecialchars( $this->getFullURL( $query, $query2 ) ); 01662 } 01663 01678 public function getInternalURL( $query = '', $query2 = false ) { 01679 global $wgInternalServer, $wgServer; 01680 $query = self::fixUrlQueryArgs( $query, $query2 ); 01681 $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; 01682 $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP ); 01683 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); 01684 return $url; 01685 } 01686 01700 public function getCanonicalURL( $query = '', $query2 = false ) { 01701 $query = self::fixUrlQueryArgs( $query, $query2 ); 01702 $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL ); 01703 wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) ); 01704 return $url; 01705 } 01706 01717 public function escapeCanonicalURL( $query = '', $query2 = false ) { 01718 wfDeprecated( __METHOD__, '1.19' ); 01719 return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) ); 01720 } 01721 01728 public function getEditURL() { 01729 if ( $this->mInterwiki != '' ) { 01730 return ''; 01731 } 01732 $s = $this->getLocalURL( 'action=edit' ); 01733 01734 return $s; 01735 } 01736 01743 public function userIsWatching() { 01744 global $wgUser; 01745 01746 if ( is_null( $this->mWatched ) ) { 01747 if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) { 01748 $this->mWatched = false; 01749 } else { 01750 $this->mWatched = $wgUser->isWatched( $this ); 01751 } 01752 } 01753 return $this->mWatched; 01754 } 01755 01763 public function userCanRead() { 01764 wfDeprecated( __METHOD__, '1.19' ); 01765 return $this->userCan( 'read' ); 01766 } 01767 01783 public function quickUserCan( $action, $user = null ) { 01784 return $this->userCan( $action, $user, false ); 01785 } 01786 01797 public function userCan( $action, $user = null, $doExpensiveQueries = true ) { 01798 if ( !$user instanceof User ) { 01799 global $wgUser; 01800 $user = $wgUser; 01801 } 01802 return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) ); 01803 } 01804 01818 public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { 01819 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); 01820 01821 // Remove the errors being ignored. 01822 foreach ( $errors as $index => $error ) { 01823 $error_key = is_array( $error ) ? $error[0] : $error; 01824 01825 if ( in_array( $error_key, $ignoreErrors ) ) { 01826 unset( $errors[$index] ); 01827 } 01828 } 01829 01830 return $errors; 01831 } 01832 01844 private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01845 if ( !wfRunHooks( 'TitleQuickPermissions', array( $this, $user, $action, &$errors, $doExpensiveQueries, $short ) ) ) { 01846 return $errors; 01847 } 01848 01849 if ( $action == 'create' ) { 01850 if ( 01851 ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || 01852 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) 01853 ) { 01854 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); 01855 } 01856 } elseif ( $action == 'move' ) { 01857 if ( !$user->isAllowed( 'move-rootuserpages' ) 01858 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01859 // Show user page-specific message only if the user can move other pages 01860 $errors[] = array( 'cant-move-user-page' ); 01861 } 01862 01863 // Check if user is allowed to move files if it's a file 01864 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { 01865 $errors[] = array( 'movenotallowedfile' ); 01866 } 01867 01868 if ( !$user->isAllowed( 'move' ) ) { 01869 // User can't move anything 01870 $userCanMove = User::groupHasPermission( 'user', 'move' ); 01871 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); 01872 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { 01873 // custom message if logged-in users without any special rights can move 01874 $errors[] = array( 'movenologintext' ); 01875 } else { 01876 $errors[] = array( 'movenotallowed' ); 01877 } 01878 } 01879 } elseif ( $action == 'move-target' ) { 01880 if ( !$user->isAllowed( 'move' ) ) { 01881 // User can't move anything 01882 $errors[] = array( 'movenotallowed' ); 01883 } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 01884 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01885 // Show user page-specific message only if the user can move other pages 01886 $errors[] = array( 'cant-move-to-user-page' ); 01887 } 01888 } elseif ( !$user->isAllowed( $action ) ) { 01889 $errors[] = $this->missingPermissionError( $action, $short ); 01890 } 01891 01892 return $errors; 01893 } 01894 01903 private function resultToError( $errors, $result ) { 01904 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { 01905 // A single array representing an error 01906 $errors[] = $result; 01907 } elseif ( is_array( $result ) && is_array( $result[0] ) ) { 01908 // A nested array representing multiple errors 01909 $errors = array_merge( $errors, $result ); 01910 } elseif ( $result !== '' && is_string( $result ) ) { 01911 // A string representing a message-id 01912 $errors[] = array( $result ); 01913 } elseif ( $result === false ) { 01914 // a generic "We don't want them to do that" 01915 $errors[] = array( 'badaccess-group0' ); 01916 } 01917 return $errors; 01918 } 01919 01931 private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { 01932 // Use getUserPermissionsErrors instead 01933 $result = ''; 01934 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { 01935 return $result ? array() : array( array( 'badaccess-group0' ) ); 01936 } 01937 // Check getUserPermissionsErrors hook 01938 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { 01939 $errors = $this->resultToError( $errors, $result ); 01940 } 01941 // Check getUserPermissionsErrorsExpensive hook 01942 if ( 01943 $doExpensiveQueries 01944 && !( $short && count( $errors ) > 0 ) 01945 && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) 01946 ) { 01947 $errors = $this->resultToError( $errors, $result ); 01948 } 01949 01950 return $errors; 01951 } 01952 01964 private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01965 # Only 'createaccount' can be performed on special pages, 01966 # which don't actually exist in the DB. 01967 if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) { 01968 $errors[] = array( 'ns-specialprotected' ); 01969 } 01970 01971 # Check $wgNamespaceProtection for restricted namespaces 01972 if ( $this->isNamespaceProtected( $user ) ) { 01973 $ns = $this->mNamespace == NS_MAIN ? 01974 wfMessage( 'nstab-main' )->text() : $this->getNsText(); 01975 $errors[] = $this->mNamespace == NS_MEDIAWIKI ? 01976 array( 'protectedinterface' ) : array( 'namespaceprotected', $ns ); 01977 } 01978 01979 return $errors; 01980 } 01981 01993 private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01994 # Protect css/js subpages of user pages 01995 # XXX: this might be better using restrictions 01996 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only 01997 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) { 01998 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { 01999 if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { 02000 $errors[] = array( 'mycustomcssprotected' ); 02001 } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) { 02002 $errors[] = array( 'mycustomjsprotected' ); 02003 } 02004 } else { 02005 if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { 02006 $errors[] = array( 'customcssprotected' ); 02007 } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { 02008 $errors[] = array( 'customjsprotected' ); 02009 } 02010 } 02011 } 02012 02013 return $errors; 02014 } 02015 02029 private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02030 foreach ( $this->getRestrictions( $action ) as $right ) { 02031 // Backwards compatibility, rewrite sysop -> editprotected 02032 if ( $right == 'sysop' ) { 02033 $right = 'editprotected'; 02034 } 02035 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected 02036 if ( $right == 'autoconfirmed' ) { 02037 $right = 'editsemiprotected'; 02038 } 02039 if ( $right == '' ) { 02040 continue; 02041 } 02042 if ( !$user->isAllowed( $right ) ) { 02043 $errors[] = array( 'protectedpagetext', $right ); 02044 } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) { 02045 $errors[] = array( 'protectedpagetext', 'protect' ); 02046 } 02047 } 02048 02049 return $errors; 02050 } 02051 02063 private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02064 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { 02065 # We /could/ use the protection level on the source page, but it's 02066 # fairly ugly as we have to establish a precedence hierarchy for pages 02067 # included by multiple cascade-protected pages. So just restrict 02068 # it to people with 'protect' permission, as they could remove the 02069 # protection anyway. 02070 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); 02071 # Cascading protection depends on more than this page... 02072 # Several cascading protected pages may include this page... 02073 # Check each cascading level 02074 # This is only for protection restrictions, not for all actions 02075 if ( isset( $restrictions[$action] ) ) { 02076 foreach ( $restrictions[$action] as $right ) { 02077 // Backwards compatibility, rewrite sysop -> editprotected 02078 if ( $right == 'sysop' ) { 02079 $right = 'editprotected'; 02080 } 02081 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected 02082 if ( $right == 'autoconfirmed' ) { 02083 $right = 'editsemiprotected'; 02084 } 02085 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { 02086 $pages = ''; 02087 foreach ( $cascadingSources as $page ) { 02088 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; 02089 } 02090 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); 02091 } 02092 } 02093 } 02094 } 02095 02096 return $errors; 02097 } 02098 02110 private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02111 global $wgDeleteRevisionsLimit, $wgLang; 02112 02113 if ( $action == 'protect' ) { 02114 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) { 02115 // If they can't edit, they shouldn't protect. 02116 $errors[] = array( 'protect-cantedit' ); 02117 } 02118 } elseif ( $action == 'create' ) { 02119 $title_protection = $this->getTitleProtection(); 02120 if ( $title_protection ) { 02121 if ( $title_protection['pt_create_perm'] == 'sysop' ) { 02122 $title_protection['pt_create_perm'] = 'editprotected'; // B/C 02123 } 02124 if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) { 02125 $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C 02126 } 02127 if ( $title_protection['pt_create_perm'] == '' || 02128 !$user->isAllowed( $title_protection['pt_create_perm'] ) ) 02129 { 02130 $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] ); 02131 } 02132 } 02133 } elseif ( $action == 'move' ) { 02134 // Check for immobile pages 02135 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02136 // Specific message for this case 02137 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 02138 } elseif ( !$this->isMovable() ) { 02139 // Less specific message for rarer cases 02140 $errors[] = array( 'immobile-source-page' ); 02141 } 02142 } elseif ( $action == 'move-target' ) { 02143 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02144 $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); 02145 } elseif ( !$this->isMovable() ) { 02146 $errors[] = array( 'immobile-target-page' ); 02147 } 02148 } elseif ( $action == 'delete' ) { 02149 if ( $doExpensiveQueries && $wgDeleteRevisionsLimit 02150 && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() ) 02151 { 02152 $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); 02153 } 02154 } 02155 return $errors; 02156 } 02157 02169 private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { 02170 // Account creation blocks handled at userlogin. 02171 // Unblocking handled in SpecialUnblock 02172 if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { 02173 return $errors; 02174 } 02175 02176 global $wgEmailConfirmToEdit; 02177 02178 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { 02179 $errors[] = array( 'confirmedittext' ); 02180 } 02181 02182 if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) { 02183 // Don't block the user from editing their own talk page unless they've been 02184 // explicitly blocked from that too. 02185 } elseif ( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { 02186 // @todo FIXME: Pass the relevant context into this function. 02187 $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() ); 02188 } 02189 02190 return $errors; 02191 } 02192 02204 private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02205 global $wgWhitelistRead, $wgWhitelistReadRegexp; 02206 02207 $whitelisted = false; 02208 if ( User::isEveryoneAllowed( 'read' ) ) { 02209 # Shortcut for public wikis, allows skipping quite a bit of code 02210 $whitelisted = true; 02211 } elseif ( $user->isAllowed( 'read' ) ) { 02212 # If the user is allowed to read pages, he is allowed to read all pages 02213 $whitelisted = true; 02214 } elseif ( $this->isSpecial( 'Userlogin' ) 02215 || $this->isSpecial( 'ChangePassword' ) 02216 || $this->isSpecial( 'PasswordReset' ) 02217 ) { 02218 # Always grant access to the login page. 02219 # Even anons need to be able to log in. 02220 $whitelisted = true; 02221 } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { 02222 # Time to check the whitelist 02223 # Only do these checks is there's something to check against 02224 $name = $this->getPrefixedText(); 02225 $dbName = $this->getPrefixedDBkey(); 02226 02227 // Check for explicit whitelisting with and without underscores 02228 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { 02229 $whitelisted = true; 02230 } elseif ( $this->getNamespace() == NS_MAIN ) { 02231 # Old settings might have the title prefixed with 02232 # a colon for main-namespace pages 02233 if ( in_array( ':' . $name, $wgWhitelistRead ) ) { 02234 $whitelisted = true; 02235 } 02236 } elseif ( $this->isSpecialPage() ) { 02237 # If it's a special page, ditch the subpage bit and check again 02238 $name = $this->getDBkey(); 02239 list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); 02240 if ( $name ) { 02241 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); 02242 if ( in_array( $pure, $wgWhitelistRead, true ) ) { 02243 $whitelisted = true; 02244 } 02245 } 02246 } 02247 } 02248 02249 if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { 02250 $name = $this->getPrefixedText(); 02251 // Check for regex whitelisting 02252 foreach ( $wgWhitelistReadRegexp as $listItem ) { 02253 if ( preg_match( $listItem, $name ) ) { 02254 $whitelisted = true; 02255 break; 02256 } 02257 } 02258 } 02259 02260 if ( !$whitelisted ) { 02261 # If the title is not whitelisted, give extensions a chance to do so... 02262 wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); 02263 if ( !$whitelisted ) { 02264 $errors[] = $this->missingPermissionError( $action, $short ); 02265 } 02266 } 02267 02268 return $errors; 02269 } 02270 02279 private function missingPermissionError( $action, $short ) { 02280 // We avoid expensive display logic for quickUserCan's and such 02281 if ( $short ) { 02282 return array( 'badaccess-group0' ); 02283 } 02284 02285 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), 02286 User::getGroupsWithPermission( $action ) ); 02287 02288 if ( count( $groups ) ) { 02289 global $wgLang; 02290 return array( 02291 'badaccess-groups', 02292 $wgLang->commaList( $groups ), 02293 count( $groups ) 02294 ); 02295 } else { 02296 return array( 'badaccess-group0' ); 02297 } 02298 } 02299 02311 protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { 02312 wfProfileIn( __METHOD__ ); 02313 02314 # Read has special handling 02315 if ( $action == 'read' ) { 02316 $checks = array( 02317 'checkPermissionHooks', 02318 'checkReadPermissions', 02319 ); 02320 } else { 02321 $checks = array( 02322 'checkQuickPermissions', 02323 'checkPermissionHooks', 02324 'checkSpecialsAndNSPermissions', 02325 'checkCSSandJSPermissions', 02326 'checkPageRestrictions', 02327 'checkCascadingSourcesRestrictions', 02328 'checkActionPermissions', 02329 'checkUserBlock' 02330 ); 02331 } 02332 02333 $errors = array(); 02334 while ( count( $checks ) > 0 && 02335 !( $short && count( $errors ) > 0 ) ) { 02336 $method = array_shift( $checks ); 02337 $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); 02338 } 02339 02340 wfProfileOut( __METHOD__ ); 02341 return $errors; 02342 } 02343 02351 public static function getFilteredRestrictionTypes( $exists = true ) { 02352 global $wgRestrictionTypes; 02353 $types = $wgRestrictionTypes; 02354 if ( $exists ) { 02355 # Remove the create restriction for existing titles 02356 $types = array_diff( $types, array( 'create' ) ); 02357 } else { 02358 # Only the create and upload restrictions apply to non-existing titles 02359 $types = array_intersect( $types, array( 'create', 'upload' ) ); 02360 } 02361 return $types; 02362 } 02363 02369 public function getRestrictionTypes() { 02370 if ( $this->isSpecialPage() ) { 02371 return array(); 02372 } 02373 02374 $types = self::getFilteredRestrictionTypes( $this->exists() ); 02375 02376 if ( $this->getNamespace() != NS_FILE ) { 02377 # Remove the upload restriction for non-file titles 02378 $types = array_diff( $types, array( 'upload' ) ); 02379 } 02380 02381 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); 02382 02383 wfDebug( __METHOD__ . ': applicable restrictions to [[' . 02384 $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); 02385 02386 return $types; 02387 } 02388 02396 private function getTitleProtection() { 02397 // Can't protect pages in special namespaces 02398 if ( $this->getNamespace() < 0 ) { 02399 return false; 02400 } 02401 02402 // Can't protect pages that exist. 02403 if ( $this->exists() ) { 02404 return false; 02405 } 02406 02407 if ( !isset( $this->mTitleProtection ) ) { 02408 $dbr = wfGetDB( DB_SLAVE ); 02409 $res = $dbr->select( 02410 'protected_titles', 02411 array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ), 02412 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02413 __METHOD__ 02414 ); 02415 02416 // fetchRow returns false if there are no rows. 02417 $this->mTitleProtection = $dbr->fetchRow( $res ); 02418 } 02419 return $this->mTitleProtection; 02420 } 02421 02431 public function updateTitleProtection( $create_perm, $reason, $expiry ) { 02432 wfDeprecated( __METHOD__, '1.19' ); 02433 02434 global $wgUser; 02435 02436 $limit = array( 'create' => $create_perm ); 02437 $expiry = array( 'create' => $expiry ); 02438 02439 $page = WikiPage::factory( $this ); 02440 $cascade = false; 02441 $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $wgUser ); 02442 02443 return $status->isOK(); 02444 } 02445 02449 public function deleteTitleProtection() { 02450 $dbw = wfGetDB( DB_MASTER ); 02451 02452 $dbw->delete( 02453 'protected_titles', 02454 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02455 __METHOD__ 02456 ); 02457 $this->mTitleProtection = false; 02458 } 02459 02467 public function isSemiProtected( $action = 'edit' ) { 02468 global $wgSemiprotectedRestrictionLevels; 02469 02470 $restrictions = $this->getRestrictions( $action ); 02471 $semi = $wgSemiprotectedRestrictionLevels; 02472 if ( !$restrictions || !$semi ) { 02473 // Not protected, or all protection is full protection 02474 return false; 02475 } 02476 02477 // Remap autoconfirmed to editsemiprotected for BC 02478 foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) { 02479 $semi[$key] = 'editsemiprotected'; 02480 } 02481 foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) { 02482 $restrictions[$key] = 'editsemiprotected'; 02483 } 02484 02485 return !array_diff( $restrictions, $semi ); 02486 } 02487 02495 public function isProtected( $action = '' ) { 02496 global $wgRestrictionLevels; 02497 02498 $restrictionTypes = $this->getRestrictionTypes(); 02499 02500 # Special pages have inherent protection 02501 if ( $this->isSpecialPage() ) { 02502 return true; 02503 } 02504 02505 # Check regular protection levels 02506 foreach ( $restrictionTypes as $type ) { 02507 if ( $action == $type || $action == '' ) { 02508 $r = $this->getRestrictions( $type ); 02509 foreach ( $wgRestrictionLevels as $level ) { 02510 if ( in_array( $level, $r ) && $level != '' ) { 02511 return true; 02512 } 02513 } 02514 } 02515 } 02516 02517 return false; 02518 } 02519 02527 public function isNamespaceProtected( User $user ) { 02528 global $wgNamespaceProtection; 02529 02530 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { 02531 foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { 02532 if ( $right != '' && !$user->isAllowed( $right ) ) { 02533 return true; 02534 } 02535 } 02536 } 02537 return false; 02538 } 02539 02545 public function isCascadeProtected() { 02546 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); 02547 return ( $sources > 0 ); 02548 } 02549 02560 public function getCascadeProtectionSources( $getPages = true ) { 02561 global $wgContLang; 02562 $pagerestrictions = array(); 02563 02564 if ( isset( $this->mCascadeSources ) && $getPages ) { 02565 return array( $this->mCascadeSources, $this->mCascadingRestrictions ); 02566 } elseif ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) { 02567 return array( $this->mHasCascadingRestrictions, $pagerestrictions ); 02568 } 02569 02570 wfProfileIn( __METHOD__ ); 02571 02572 $dbr = wfGetDB( DB_SLAVE ); 02573 02574 if ( $this->getNamespace() == NS_FILE ) { 02575 $tables = array( 'imagelinks', 'page_restrictions' ); 02576 $where_clauses = array( 02577 'il_to' => $this->getDBkey(), 02578 'il_from=pr_page', 02579 'pr_cascade' => 1 02580 ); 02581 } else { 02582 $tables = array( 'templatelinks', 'page_restrictions' ); 02583 $where_clauses = array( 02584 'tl_namespace' => $this->getNamespace(), 02585 'tl_title' => $this->getDBkey(), 02586 'tl_from=pr_page', 02587 'pr_cascade' => 1 02588 ); 02589 } 02590 02591 if ( $getPages ) { 02592 $cols = array( 'pr_page', 'page_namespace', 'page_title', 02593 'pr_expiry', 'pr_type', 'pr_level' ); 02594 $where_clauses[] = 'page_id=pr_page'; 02595 $tables[] = 'page'; 02596 } else { 02597 $cols = array( 'pr_expiry' ); 02598 } 02599 02600 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); 02601 02602 $sources = $getPages ? array() : false; 02603 $now = wfTimestampNow(); 02604 $purgeExpired = false; 02605 02606 foreach ( $res as $row ) { 02607 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02608 if ( $expiry > $now ) { 02609 if ( $getPages ) { 02610 $page_id = $row->pr_page; 02611 $page_ns = $row->page_namespace; 02612 $page_title = $row->page_title; 02613 $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); 02614 # Add groups needed for each restriction type if its not already there 02615 # Make sure this restriction type still exists 02616 02617 if ( !isset( $pagerestrictions[$row->pr_type] ) ) { 02618 $pagerestrictions[$row->pr_type] = array(); 02619 } 02620 02621 if ( 02622 isset( $pagerestrictions[$row->pr_type] ) 02623 && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) 02624 ) { 02625 $pagerestrictions[$row->pr_type][] = $row->pr_level; 02626 } 02627 } else { 02628 $sources = true; 02629 } 02630 } else { 02631 // Trigger lazy purge of expired restrictions from the db 02632 $purgeExpired = true; 02633 } 02634 } 02635 if ( $purgeExpired ) { 02636 Title::purgeExpiredRestrictions(); 02637 } 02638 02639 if ( $getPages ) { 02640 $this->mCascadeSources = $sources; 02641 $this->mCascadingRestrictions = $pagerestrictions; 02642 } else { 02643 $this->mHasCascadingRestrictions = $sources; 02644 } 02645 02646 wfProfileOut( __METHOD__ ); 02647 return array( $sources, $pagerestrictions ); 02648 } 02649 02656 public function getRestrictions( $action ) { 02657 if ( !$this->mRestrictionsLoaded ) { 02658 $this->loadRestrictions(); 02659 } 02660 return isset( $this->mRestrictions[$action] ) 02661 ? $this->mRestrictions[$action] 02662 : array(); 02663 } 02664 02672 public function getRestrictionExpiry( $action ) { 02673 if ( !$this->mRestrictionsLoaded ) { 02674 $this->loadRestrictions(); 02675 } 02676 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; 02677 } 02678 02684 function areRestrictionsCascading() { 02685 if ( !$this->mRestrictionsLoaded ) { 02686 $this->loadRestrictions(); 02687 } 02688 02689 return $this->mCascadeRestriction; 02690 } 02691 02699 private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { 02700 $rows = array(); 02701 02702 foreach ( $res as $row ) { 02703 $rows[] = $row; 02704 } 02705 02706 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); 02707 } 02708 02718 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { 02719 global $wgContLang; 02720 $dbr = wfGetDB( DB_SLAVE ); 02721 02722 $restrictionTypes = $this->getRestrictionTypes(); 02723 02724 foreach ( $restrictionTypes as $type ) { 02725 $this->mRestrictions[$type] = array(); 02726 $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); 02727 } 02728 02729 $this->mCascadeRestriction = false; 02730 02731 # Backwards-compatibility: also load the restrictions from the page record (old format). 02732 02733 if ( $oldFashionedRestrictions === null ) { 02734 $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', 02735 array( 'page_id' => $this->getArticleID() ), __METHOD__ ); 02736 } 02737 02738 if ( $oldFashionedRestrictions != '' ) { 02739 02740 foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { 02741 $temp = explode( '=', trim( $restrict ) ); 02742 if ( count( $temp ) == 1 ) { 02743 // old old format should be treated as edit/move restriction 02744 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); 02745 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); 02746 } else { 02747 $restriction = trim( $temp[1] ); 02748 if ( $restriction != '' ) { //some old entries are empty 02749 $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); 02750 } 02751 } 02752 } 02753 02754 $this->mOldRestrictions = true; 02755 02756 } 02757 02758 if ( count( $rows ) ) { 02759 # Current system - load second to make them override. 02760 $now = wfTimestampNow(); 02761 $purgeExpired = false; 02762 02763 # Cycle through all the restrictions. 02764 foreach ( $rows as $row ) { 02765 02766 // Don't take care of restrictions types that aren't allowed 02767 if ( !in_array( $row->pr_type, $restrictionTypes ) ) { 02768 continue; 02769 } 02770 02771 // This code should be refactored, now that it's being used more generally, 02772 // But I don't really see any harm in leaving it in Block for now -werdna 02773 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02774 02775 // Only apply the restrictions if they haven't expired! 02776 if ( !$expiry || $expiry > $now ) { 02777 $this->mRestrictionsExpiry[$row->pr_type] = $expiry; 02778 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); 02779 02780 $this->mCascadeRestriction |= $row->pr_cascade; 02781 } else { 02782 // Trigger a lazy purge of expired restrictions 02783 $purgeExpired = true; 02784 } 02785 } 02786 02787 if ( $purgeExpired ) { 02788 Title::purgeExpiredRestrictions(); 02789 } 02790 } 02791 02792 $this->mRestrictionsLoaded = true; 02793 } 02794 02801 public function loadRestrictions( $oldFashionedRestrictions = null ) { 02802 global $wgContLang; 02803 if ( !$this->mRestrictionsLoaded ) { 02804 if ( $this->exists() ) { 02805 $dbr = wfGetDB( DB_SLAVE ); 02806 02807 $res = $dbr->select( 02808 'page_restrictions', 02809 array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ), 02810 array( 'pr_page' => $this->getArticleID() ), 02811 __METHOD__ 02812 ); 02813 02814 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); 02815 } else { 02816 $title_protection = $this->getTitleProtection(); 02817 02818 if ( $title_protection ) { 02819 $now = wfTimestampNow(); 02820 $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); 02821 02822 if ( !$expiry || $expiry > $now ) { 02823 // Apply the restrictions 02824 $this->mRestrictionsExpiry['create'] = $expiry; 02825 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); 02826 } else { // Get rid of the old restrictions 02827 Title::purgeExpiredRestrictions(); 02828 $this->mTitleProtection = false; 02829 } 02830 } else { 02831 $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); 02832 } 02833 $this->mRestrictionsLoaded = true; 02834 } 02835 } 02836 } 02837 02842 public function flushRestrictions() { 02843 $this->mRestrictionsLoaded = false; 02844 $this->mTitleProtection = null; 02845 } 02846 02850 static function purgeExpiredRestrictions() { 02851 if ( wfReadOnly() ) { 02852 return; 02853 } 02854 02855 $method = __METHOD__; 02856 $dbw = wfGetDB( DB_MASTER ); 02857 $dbw->onTransactionIdle( function() use ( $dbw, $method ) { 02858 $dbw->delete( 02859 'page_restrictions', 02860 array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02861 $method 02862 ); 02863 $dbw->delete( 02864 'protected_titles', 02865 array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02866 $method 02867 ); 02868 } ); 02869 } 02870 02876 public function hasSubpages() { 02877 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 02878 # Duh 02879 return false; 02880 } 02881 02882 # We dynamically add a member variable for the purpose of this method 02883 # alone to cache the result. There's no point in having it hanging 02884 # around uninitialized in every Title object; therefore we only add it 02885 # if needed and don't declare it statically. 02886 if ( isset( $this->mHasSubpages ) ) { 02887 return $this->mHasSubpages; 02888 } 02889 02890 $subpages = $this->getSubpages( 1 ); 02891 if ( $subpages instanceof TitleArray ) { 02892 return $this->mHasSubpages = (bool)$subpages->count(); 02893 } 02894 return $this->mHasSubpages = false; 02895 } 02896 02904 public function getSubpages( $limit = -1 ) { 02905 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 02906 return array(); 02907 } 02908 02909 $dbr = wfGetDB( DB_SLAVE ); 02910 $conds['page_namespace'] = $this->getNamespace(); 02911 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); 02912 $options = array(); 02913 if ( $limit > -1 ) { 02914 $options['LIMIT'] = $limit; 02915 } 02916 return $this->mSubpages = TitleArray::newFromResult( 02917 $dbr->select( 'page', 02918 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), 02919 $conds, 02920 __METHOD__, 02921 $options 02922 ) 02923 ); 02924 } 02925 02931 public function isDeleted() { 02932 if ( $this->getNamespace() < 0 ) { 02933 $n = 0; 02934 } else { 02935 $dbr = wfGetDB( DB_SLAVE ); 02936 02937 $n = $dbr->selectField( 'archive', 'COUNT(*)', 02938 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02939 __METHOD__ 02940 ); 02941 if ( $this->getNamespace() == NS_FILE ) { 02942 $n += $dbr->selectField( 'filearchive', 'COUNT(*)', 02943 array( 'fa_name' => $this->getDBkey() ), 02944 __METHOD__ 02945 ); 02946 } 02947 } 02948 return (int)$n; 02949 } 02950 02956 public function isDeletedQuick() { 02957 if ( $this->getNamespace() < 0 ) { 02958 return false; 02959 } 02960 $dbr = wfGetDB( DB_SLAVE ); 02961 $deleted = (bool)$dbr->selectField( 'archive', '1', 02962 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02963 __METHOD__ 02964 ); 02965 if ( !$deleted && $this->getNamespace() == NS_FILE ) { 02966 $deleted = (bool)$dbr->selectField( 'filearchive', '1', 02967 array( 'fa_name' => $this->getDBkey() ), 02968 __METHOD__ 02969 ); 02970 } 02971 return $deleted; 02972 } 02973 02982 public function getArticleID( $flags = 0 ) { 02983 if ( $this->getNamespace() < 0 ) { 02984 return $this->mArticleID = 0; 02985 } 02986 $linkCache = LinkCache::singleton(); 02987 if ( $flags & self::GAID_FOR_UPDATE ) { 02988 $oldUpdate = $linkCache->forUpdate( true ); 02989 $linkCache->clearLink( $this ); 02990 $this->mArticleID = $linkCache->addLinkObj( $this ); 02991 $linkCache->forUpdate( $oldUpdate ); 02992 } else { 02993 if ( -1 == $this->mArticleID ) { 02994 $this->mArticleID = $linkCache->addLinkObj( $this ); 02995 } 02996 } 02997 return $this->mArticleID; 02998 } 02999 03007 public function isRedirect( $flags = 0 ) { 03008 if ( !is_null( $this->mRedirect ) ) { 03009 return $this->mRedirect; 03010 } 03011 # Calling getArticleID() loads the field from cache as needed 03012 if ( !$this->getArticleID( $flags ) ) { 03013 return $this->mRedirect = false; 03014 } 03015 03016 $linkCache = LinkCache::singleton(); 03017 $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' ); 03018 if ( $cached === null ) { 03019 # Trust LinkCache's state over our own 03020 # LinkCache is telling us that the page doesn't exist, despite there being cached 03021 # data relating to an existing page in $this->mArticleID. Updaters should clear 03022 # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is 03023 # set, then LinkCache will definitely be up to date here, since getArticleID() forces 03024 # LinkCache to refresh its data from the master. 03025 return $this->mRedirect = false; 03026 } 03027 03028 $this->mRedirect = (bool)$cached; 03029 03030 return $this->mRedirect; 03031 } 03032 03040 public function getLength( $flags = 0 ) { 03041 if ( $this->mLength != -1 ) { 03042 return $this->mLength; 03043 } 03044 # Calling getArticleID() loads the field from cache as needed 03045 if ( !$this->getArticleID( $flags ) ) { 03046 return $this->mLength = 0; 03047 } 03048 $linkCache = LinkCache::singleton(); 03049 $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' ); 03050 if ( $cached === null ) { 03051 # Trust LinkCache's state over our own, as for isRedirect() 03052 return $this->mLength = 0; 03053 } 03054 03055 $this->mLength = intval( $cached ); 03056 03057 return $this->mLength; 03058 } 03059 03066 public function getLatestRevID( $flags = 0 ) { 03067 if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) { 03068 return intval( $this->mLatestID ); 03069 } 03070 # Calling getArticleID() loads the field from cache as needed 03071 if ( !$this->getArticleID( $flags ) ) { 03072 return $this->mLatestID = 0; 03073 } 03074 $linkCache = LinkCache::singleton(); 03075 $linkCache->addLinkObj( $this ); 03076 $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' ); 03077 if ( $cached === null ) { 03078 # Trust LinkCache's state over our own, as for isRedirect() 03079 return $this->mLatestID = 0; 03080 } 03081 03082 $this->mLatestID = intval( $cached ); 03083 03084 return $this->mLatestID; 03085 } 03086 03097 public function resetArticleID( $newid ) { 03098 $linkCache = LinkCache::singleton(); 03099 $linkCache->clearLink( $this ); 03100 03101 if ( $newid === false ) { 03102 $this->mArticleID = -1; 03103 } else { 03104 $this->mArticleID = intval( $newid ); 03105 } 03106 $this->mRestrictionsLoaded = false; 03107 $this->mRestrictions = array(); 03108 $this->mRedirect = null; 03109 $this->mLength = -1; 03110 $this->mLatestID = false; 03111 $this->mContentModel = false; 03112 $this->mEstimateRevisions = null; 03113 } 03114 03122 public static function capitalize( $text, $ns = NS_MAIN ) { 03123 global $wgContLang; 03124 03125 if ( MWNamespace::isCapitalized( $ns ) ) { 03126 return $wgContLang->ucfirst( $text ); 03127 } else { 03128 return $text; 03129 } 03130 } 03131 03143 private function secureAndSplit() { 03144 global $wgContLang, $wgLocalInterwiki; 03145 03146 # Initialisation 03147 $this->mInterwiki = $this->mFragment = ''; 03148 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN 03149 03150 $dbkey = $this->mDbkeyform; 03151 03152 # Strip Unicode bidi override characters. 03153 # Sometimes they slip into cut-n-pasted page titles, where the 03154 # override chars get included in list displays. 03155 $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey ); 03156 03157 # Clean up whitespace 03158 # Note: use of the /u option on preg_replace here will cause 03159 # input with invalid UTF-8 sequences to be nullified out in PHP 5.2.x, 03160 # conveniently disabling them. 03161 $dbkey = preg_replace( '/[ _\xA0\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u', '_', $dbkey ); 03162 $dbkey = trim( $dbkey, '_' ); 03163 03164 if ( $dbkey == '' ) { 03165 return false; 03166 } 03167 03168 if ( strpos( $dbkey, UTF8_REPLACEMENT ) !== false ) { 03169 # Contained illegal UTF-8 sequences or forbidden Unicode chars. 03170 return false; 03171 } 03172 03173 $this->mDbkeyform = $dbkey; 03174 03175 # Initial colon indicates main namespace rather than specified default 03176 # but should not create invalid {ns,title} pairs such as {0,Project:Foo} 03177 if ( ':' == $dbkey[0] ) { 03178 $this->mNamespace = NS_MAIN; 03179 $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing 03180 $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace 03181 } 03182 03183 # Namespace or interwiki prefix 03184 $firstPass = true; 03185 $prefixRegexp = "/^(.+?)_*:_*(.*)$/S"; 03186 do { 03187 $m = array(); 03188 if ( preg_match( $prefixRegexp, $dbkey, $m ) ) { 03189 $p = $m[1]; 03190 if ( ( $ns = $wgContLang->getNsIndex( $p ) ) !== false ) { 03191 # Ordinary namespace 03192 $dbkey = $m[2]; 03193 $this->mNamespace = $ns; 03194 # For Talk:X pages, check if X has a "namespace" prefix 03195 if ( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) { 03196 if ( $wgContLang->getNsIndex( $x[1] ) ) { 03197 # Disallow Talk:File:x type titles... 03198 return false; 03199 } elseif ( Interwiki::isValidInterwiki( $x[1] ) ) { 03200 # Disallow Talk:Interwiki:x type titles... 03201 return false; 03202 } 03203 } 03204 } elseif ( Interwiki::isValidInterwiki( $p ) ) { 03205 if ( !$firstPass ) { 03206 # Can't make a local interwiki link to an interwiki link. 03207 # That's just crazy! 03208 return false; 03209 } 03210 03211 # Interwiki link 03212 $dbkey = $m[2]; 03213 $this->mInterwiki = $wgContLang->lc( $p ); 03214 03215 # Redundant interwiki prefix to the local wiki 03216 if ( $wgLocalInterwiki !== false 03217 && 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) 03218 { 03219 if ( $dbkey == '' ) { 03220 # Can't have an empty self-link 03221 return false; 03222 } 03223 $this->mInterwiki = ''; 03224 $firstPass = false; 03225 # Do another namespace split... 03226 continue; 03227 } 03228 03229 # If there's an initial colon after the interwiki, that also 03230 # resets the default namespace 03231 if ( $dbkey !== '' && $dbkey[0] == ':' ) { 03232 $this->mNamespace = NS_MAIN; 03233 $dbkey = substr( $dbkey, 1 ); 03234 } 03235 } 03236 # If there's no recognized interwiki or namespace, 03237 # then let the colon expression be part of the title. 03238 } 03239 break; 03240 } while ( true ); 03241 03242 # We already know that some pages won't be in the database! 03243 if ( $this->mInterwiki != '' || NS_SPECIAL == $this->mNamespace ) { 03244 $this->mArticleID = 0; 03245 } 03246 $fragment = strstr( $dbkey, '#' ); 03247 if ( false !== $fragment ) { 03248 $this->setFragment( $fragment ); 03249 $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) ); 03250 # remove whitespace again: prevents "Foo_bar_#" 03251 # becoming "Foo_bar_" 03252 $dbkey = preg_replace( '/_*$/', '', $dbkey ); 03253 } 03254 03255 # Reject illegal characters. 03256 $rxTc = self::getTitleInvalidRegex(); 03257 if ( preg_match( $rxTc, $dbkey ) ) { 03258 return false; 03259 } 03260 03261 # Pages with "/./" or "/../" appearing in the URLs will often be un- 03262 # reachable due to the way web browsers deal with 'relative' URLs. 03263 # Also, they conflict with subpage syntax. Forbid them explicitly. 03264 if ( 03265 strpos( $dbkey, '.' ) !== false && 03266 ( 03267 $dbkey === '.' || $dbkey === '..' || 03268 strpos( $dbkey, './' ) === 0 || 03269 strpos( $dbkey, '../' ) === 0 || 03270 strpos( $dbkey, '/./' ) !== false || 03271 strpos( $dbkey, '/../' ) !== false || 03272 substr( $dbkey, -2 ) == '/.' || 03273 substr( $dbkey, -3 ) == '/..' 03274 ) 03275 ) { 03276 return false; 03277 } 03278 03279 # Magic tilde sequences? Nu-uh! 03280 if ( strpos( $dbkey, '~~~' ) !== false ) { 03281 return false; 03282 } 03283 03284 # Limit the size of titles to 255 bytes. This is typically the size of the 03285 # underlying database field. We make an exception for special pages, which 03286 # don't need to be stored in the database, and may edge over 255 bytes due 03287 # to subpage syntax for long titles, e.g. [[Special:Block/Long name]] 03288 if ( 03289 ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) 03290 || strlen( $dbkey ) > 512 03291 ) { 03292 return false; 03293 } 03294 03295 # Normally, all wiki links are forced to have an initial capital letter so [[foo]] 03296 # and [[Foo]] point to the same place. Don't force it for interwikis, since the 03297 # other site might be case-sensitive. 03298 $this->mUserCaseDBKey = $dbkey; 03299 if ( $this->mInterwiki == '' ) { 03300 $dbkey = self::capitalize( $dbkey, $this->mNamespace ); 03301 } 03302 03303 # Can't make a link to a namespace alone... "empty" local links can only be 03304 # self-links with a fragment identifier. 03305 # TODO: Why do we exclude NS_MAIN (bug 54044) 03306 if ( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) { 03307 return false; 03308 } 03309 03310 // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles. 03311 // IP names are not allowed for accounts, and can only be referring to 03312 // edits from the IP. Given '::' abbreviations and caps/lowercaps, 03313 // there are numerous ways to present the same IP. Having sp:contribs scan 03314 // them all is silly and having some show the edits and others not is 03315 // inconsistent. Same for talk/userpages. Keep them normalized instead. 03316 $dbkey = ( $this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK ) 03317 ? IP::sanitizeIP( $dbkey ) 03318 : $dbkey; 03319 03320 // Any remaining initial :s are illegal. 03321 if ( $dbkey !== '' && ':' == $dbkey[0] ) { 03322 return false; 03323 } 03324 03325 # Fill fields 03326 $this->mDbkeyform = $dbkey; 03327 $this->mUrlform = wfUrlencode( $dbkey ); 03328 03329 $this->mTextform = str_replace( '_', ' ', $dbkey ); 03330 03331 return true; 03332 } 03333 03346 public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03347 if ( count( $options ) > 0 ) { 03348 $db = wfGetDB( DB_MASTER ); 03349 } else { 03350 $db = wfGetDB( DB_SLAVE ); 03351 } 03352 03353 $res = $db->select( 03354 array( 'page', $table ), 03355 self::getSelectFields(), 03356 array( 03357 "{$prefix}_from=page_id", 03358 "{$prefix}_namespace" => $this->getNamespace(), 03359 "{$prefix}_title" => $this->getDBkey() ), 03360 __METHOD__, 03361 $options 03362 ); 03363 03364 $retVal = array(); 03365 if ( $res->numRows() ) { 03366 $linkCache = LinkCache::singleton(); 03367 foreach ( $res as $row ) { 03368 $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ); 03369 if ( $titleObj ) { 03370 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03371 $retVal[] = $titleObj; 03372 } 03373 } 03374 } 03375 return $retVal; 03376 } 03377 03388 public function getTemplateLinksTo( $options = array() ) { 03389 return $this->getLinksTo( $options, 'templatelinks', 'tl' ); 03390 } 03391 03404 public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03405 global $wgContentHandlerUseDB; 03406 03407 $id = $this->getArticleID(); 03408 03409 # If the page doesn't exist; there can't be any link from this page 03410 if ( !$id ) { 03411 return array(); 03412 } 03413 03414 if ( count( $options ) > 0 ) { 03415 $db = wfGetDB( DB_MASTER ); 03416 } else { 03417 $db = wfGetDB( DB_SLAVE ); 03418 } 03419 03420 $namespaceFiled = "{$prefix}_namespace"; 03421 $titleField = "{$prefix}_title"; 03422 03423 $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ); 03424 if ( $wgContentHandlerUseDB ) { 03425 $fields[] = 'page_content_model'; 03426 } 03427 03428 $res = $db->select( 03429 array( $table, 'page' ), 03430 $fields, 03431 array( "{$prefix}_from" => $id ), 03432 __METHOD__, 03433 $options, 03434 array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) ) 03435 ); 03436 03437 $retVal = array(); 03438 if ( $res->numRows() ) { 03439 $linkCache = LinkCache::singleton(); 03440 foreach ( $res as $row ) { 03441 $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); 03442 if ( $titleObj ) { 03443 if ( $row->page_id ) { 03444 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03445 } else { 03446 $linkCache->addBadLinkObj( $titleObj ); 03447 } 03448 $retVal[] = $titleObj; 03449 } 03450 } 03451 } 03452 return $retVal; 03453 } 03454 03465 public function getTemplateLinksFrom( $options = array() ) { 03466 return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); 03467 } 03468 03475 public function getBrokenLinksFrom() { 03476 if ( $this->getArticleID() == 0 ) { 03477 # All links from article ID 0 are false positives 03478 return array(); 03479 } 03480 03481 $dbr = wfGetDB( DB_SLAVE ); 03482 $res = $dbr->select( 03483 array( 'page', 'pagelinks' ), 03484 array( 'pl_namespace', 'pl_title' ), 03485 array( 03486 'pl_from' => $this->getArticleID(), 03487 'page_namespace IS NULL' 03488 ), 03489 __METHOD__, array(), 03490 array( 03491 'page' => array( 03492 'LEFT JOIN', 03493 array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) 03494 ) 03495 ) 03496 ); 03497 03498 $retVal = array(); 03499 foreach ( $res as $row ) { 03500 $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); 03501 } 03502 return $retVal; 03503 } 03504 03511 public function getSquidURLs() { 03512 $urls = array( 03513 $this->getInternalURL(), 03514 $this->getInternalURL( 'action=history' ) 03515 ); 03516 03517 $pageLang = $this->getPageLanguage(); 03518 if ( $pageLang->hasVariants() ) { 03519 $variants = $pageLang->getVariants(); 03520 foreach ( $variants as $vCode ) { 03521 $urls[] = $this->getInternalURL( '', $vCode ); 03522 } 03523 } 03524 03525 wfRunHooks( 'TitleSquidURLs', array( $this, &$urls ) ); 03526 return $urls; 03527 } 03528 03532 public function purgeSquid() { 03533 global $wgUseSquid; 03534 if ( $wgUseSquid ) { 03535 $urls = $this->getSquidURLs(); 03536 $u = new SquidUpdate( $urls ); 03537 $u->doUpdate(); 03538 } 03539 } 03540 03547 public function moveNoAuth( &$nt ) { 03548 return $this->moveTo( $nt, false ); 03549 } 03550 03561 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { 03562 global $wgUser, $wgContentHandlerUseDB; 03563 03564 $errors = array(); 03565 if ( !$nt ) { 03566 // Normally we'd add this to $errors, but we'll get 03567 // lots of syntax errors if $nt is not an object 03568 return array( array( 'badtitletext' ) ); 03569 } 03570 if ( $this->equals( $nt ) ) { 03571 $errors[] = array( 'selfmove' ); 03572 } 03573 if ( !$this->isMovable() ) { 03574 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 03575 } 03576 if ( $nt->getInterwiki() != '' ) { 03577 $errors[] = array( 'immobile-target-namespace-iw' ); 03578 } 03579 if ( !$nt->isMovable() ) { 03580 $errors[] = array( 'immobile-target-namespace', $nt->getNsText() ); 03581 } 03582 03583 $oldid = $this->getArticleID(); 03584 $newid = $nt->getArticleID(); 03585 03586 if ( strlen( $nt->getDBkey() ) < 1 ) { 03587 $errors[] = array( 'articleexists' ); 03588 } 03589 if ( 03590 ( $this->getDBkey() == '' ) || 03591 ( !$oldid ) || 03592 ( $nt->getDBkey() == '' ) 03593 ) { 03594 $errors[] = array( 'badarticleerror' ); 03595 } 03596 03597 // Content model checks 03598 if ( !$wgContentHandlerUseDB && 03599 $this->getContentModel() !== $nt->getContentModel() ) { 03600 // can't move a page if that would change the page's content model 03601 $errors[] = array( 03602 'bad-target-model', 03603 ContentHandler::getLocalizedName( $this->getContentModel() ), 03604 ContentHandler::getLocalizedName( $nt->getContentModel() ) 03605 ); 03606 } 03607 03608 // Image-specific checks 03609 if ( $this->getNamespace() == NS_FILE ) { 03610 $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) ); 03611 } 03612 03613 if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) { 03614 $errors[] = array( 'nonfile-cannot-move-to-file' ); 03615 } 03616 03617 if ( $auth ) { 03618 $errors = wfMergeErrorArrays( $errors, 03619 $this->getUserPermissionsErrors( 'move', $wgUser ), 03620 $this->getUserPermissionsErrors( 'edit', $wgUser ), 03621 $nt->getUserPermissionsErrors( 'move-target', $wgUser ), 03622 $nt->getUserPermissionsErrors( 'edit', $wgUser ) ); 03623 } 03624 03625 $match = EditPage::matchSummarySpamRegex( $reason ); 03626 if ( $match !== false ) { 03627 // This is kind of lame, won't display nice 03628 $errors[] = array( 'spamprotectiontext' ); 03629 } 03630 03631 $err = null; 03632 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) { 03633 $errors[] = array( 'hookaborted', $err ); 03634 } 03635 03636 # The move is allowed only if (1) the target doesn't exist, or 03637 # (2) the target is a redirect to the source, and has no history 03638 # (so we can undo bad moves right after they're done). 03639 03640 if ( 0 != $newid ) { # Target exists; check for validity 03641 if ( !$this->isValidMoveTarget( $nt ) ) { 03642 $errors[] = array( 'articleexists' ); 03643 } 03644 } else { 03645 $tp = $nt->getTitleProtection(); 03646 $right = $tp['pt_create_perm']; 03647 if ( $right == 'sysop' ) { 03648 $right = 'editprotected'; // B/C 03649 } 03650 if ( $right == 'autoconfirmed' ) { 03651 $right = 'editsemiprotected'; // B/C 03652 } 03653 if ( $tp and !$wgUser->isAllowed( $right ) ) { 03654 $errors[] = array( 'cantmove-titleprotected' ); 03655 } 03656 } 03657 if ( empty( $errors ) ) { 03658 return true; 03659 } 03660 return $errors; 03661 } 03662 03668 protected function validateFileMoveOperation( $nt ) { 03669 global $wgUser; 03670 03671 $errors = array(); 03672 03673 // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below 03674 03675 $file = wfLocalFile( $this ); 03676 if ( $file->exists() ) { 03677 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { 03678 $errors[] = array( 'imageinvalidfilename' ); 03679 } 03680 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) { 03681 $errors[] = array( 'imagetypemismatch' ); 03682 } 03683 } 03684 03685 if ( $nt->getNamespace() != NS_FILE ) { 03686 $errors[] = array( 'imagenocrossnamespace' ); 03687 // From here we want to do checks on a file object, so if we can't 03688 // create one, we must return. 03689 return $errors; 03690 } 03691 03692 // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here 03693 03694 $destFile = wfLocalFile( $nt ); 03695 if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) { 03696 $errors[] = array( 'file-exists-sharedrepo' ); 03697 } 03698 03699 return $errors; 03700 } 03701 03713 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { 03714 global $wgUser; 03715 $err = $this->isValidMoveOperation( $nt, $auth, $reason ); 03716 if ( is_array( $err ) ) { 03717 // Auto-block user's IP if the account was "hard" blocked 03718 $wgUser->spreadAnyEditBlock(); 03719 return $err; 03720 } 03721 // Check suppressredirect permission 03722 if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) { 03723 $createRedirect = true; 03724 } 03725 03726 wfRunHooks( 'TitleMove', array( $this, $nt, $wgUser ) ); 03727 03728 // If it is a file, move it first. 03729 // It is done before all other moving stuff is done because it's hard to revert. 03730 $dbw = wfGetDB( DB_MASTER ); 03731 if ( $this->getNamespace() == NS_FILE ) { 03732 $file = wfLocalFile( $this ); 03733 if ( $file->exists() ) { 03734 $status = $file->move( $nt ); 03735 if ( !$status->isOk() ) { 03736 return $status->getErrorsArray(); 03737 } 03738 } 03739 // Clear RepoGroup process cache 03740 RepoGroup::singleton()->clearCache( $this ); 03741 RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache 03742 } 03743 03744 $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own. 03745 $pageid = $this->getArticleID( self::GAID_FOR_UPDATE ); 03746 $protected = $this->isProtected(); 03747 03748 // Do the actual move 03749 $this->moveToInternal( $nt, $reason, $createRedirect ); 03750 03751 // Refresh the sortkey for this row. Be careful to avoid resetting 03752 // cl_timestamp, which may disturb time-based lists on some sites. 03753 $prefixes = $dbw->select( 03754 'categorylinks', 03755 array( 'cl_sortkey_prefix', 'cl_to' ), 03756 array( 'cl_from' => $pageid ), 03757 __METHOD__ 03758 ); 03759 foreach ( $prefixes as $prefixRow ) { 03760 $prefix = $prefixRow->cl_sortkey_prefix; 03761 $catTo = $prefixRow->cl_to; 03762 $dbw->update( 'categorylinks', 03763 array( 03764 'cl_sortkey' => Collation::singleton()->getSortKey( 03765 $nt->getCategorySortkey( $prefix ) ), 03766 'cl_timestamp=cl_timestamp' ), 03767 array( 03768 'cl_from' => $pageid, 03769 'cl_to' => $catTo ), 03770 __METHOD__ 03771 ); 03772 } 03773 03774 $redirid = $this->getArticleID(); 03775 03776 if ( $protected ) { 03777 # Protect the redirect title as the title used to be... 03778 $dbw->insertSelect( 'page_restrictions', 'page_restrictions', 03779 array( 03780 'pr_page' => $redirid, 03781 'pr_type' => 'pr_type', 03782 'pr_level' => 'pr_level', 03783 'pr_cascade' => 'pr_cascade', 03784 'pr_user' => 'pr_user', 03785 'pr_expiry' => 'pr_expiry' 03786 ), 03787 array( 'pr_page' => $pageid ), 03788 __METHOD__, 03789 array( 'IGNORE' ) 03790 ); 03791 # Update the protection log 03792 $log = new LogPage( 'protect' ); 03793 $comment = wfMessage( 03794 'prot_1movedto2', 03795 $this->getPrefixedText(), 03796 $nt->getPrefixedText() 03797 )->inContentLanguage()->text(); 03798 if ( $reason ) { 03799 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03800 } 03801 // @todo FIXME: $params? 03802 $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ), $wgUser ); 03803 } 03804 03805 # Update watchlists 03806 $oldnamespace = MWNamespace::getSubject( $this->getNamespace() ); 03807 $newnamespace = MWNamespace::getSubject( $nt->getNamespace() ); 03808 $oldtitle = $this->getDBkey(); 03809 $newtitle = $nt->getDBkey(); 03810 03811 if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) { 03812 WatchedItem::duplicateEntries( $this, $nt ); 03813 } 03814 03815 $dbw->commit( __METHOD__ ); 03816 03817 wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); 03818 return true; 03819 } 03820 03831 private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) { 03832 global $wgUser, $wgContLang; 03833 03834 if ( $nt->exists() ) { 03835 $moveOverRedirect = true; 03836 $logType = 'move_redir'; 03837 } else { 03838 $moveOverRedirect = false; 03839 $logType = 'move'; 03840 } 03841 03842 if ( $createRedirect ) { 03843 $contentHandler = ContentHandler::getForTitle( $this ); 03844 $redirectContent = $contentHandler->makeRedirectContent( $nt, 03845 wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() ); 03846 03847 // NOTE: If this page's content model does not support redirects, $redirectContent will be null. 03848 } else { 03849 $redirectContent = null; 03850 } 03851 03852 $logEntry = new ManualLogEntry( 'move', $logType ); 03853 $logEntry->setPerformer( $wgUser ); 03854 $logEntry->setTarget( $this ); 03855 $logEntry->setComment( $reason ); 03856 $logEntry->setParameters( array( 03857 '4::target' => $nt->getPrefixedText(), 03858 '5::noredir' => $redirectContent ? '0': '1', 03859 ) ); 03860 03861 $formatter = LogFormatter::newFromEntry( $logEntry ); 03862 $formatter->setContext( RequestContext::newExtraneousContext( $this ) ); 03863 $comment = $formatter->getPlainActionText(); 03864 if ( $reason ) { 03865 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03866 } 03867 # Truncate for whole multibyte characters. 03868 $comment = $wgContLang->truncate( $comment, 255 ); 03869 03870 $oldid = $this->getArticleID(); 03871 03872 $dbw = wfGetDB( DB_MASTER ); 03873 03874 $newpage = WikiPage::factory( $nt ); 03875 03876 if ( $moveOverRedirect ) { 03877 $newid = $nt->getArticleID(); 03878 03879 # Delete the old redirect. We don't save it to history since 03880 # by definition if we've got here it's rather uninteresting. 03881 # We have to remove it so that the next step doesn't trigger 03882 # a conflict on the unique namespace+title index... 03883 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ ); 03884 03885 $newpage->doDeleteUpdates( $newid ); 03886 } 03887 03888 # Save a null revision in the page's history notifying of the move 03889 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); 03890 if ( !is_object( $nullRevision ) ) { 03891 throw new MWException( 'No valid null revision produced in ' . __METHOD__ ); 03892 } 03893 03894 $nullRevision->insertOn( $dbw ); 03895 03896 # Change the name of the target page: 03897 $dbw->update( 'page', 03898 /* SET */ array( 03899 'page_namespace' => $nt->getNamespace(), 03900 'page_title' => $nt->getDBkey(), 03901 ), 03902 /* WHERE */ array( 'page_id' => $oldid ), 03903 __METHOD__ 03904 ); 03905 03906 // clean up the old title before reset article id - bug 45348 03907 if ( !$redirectContent ) { 03908 WikiPage::onArticleDelete( $this ); 03909 } 03910 03911 $this->resetArticleID( 0 ); // 0 == non existing 03912 $nt->resetArticleID( $oldid ); 03913 $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397 03914 03915 $newpage->updateRevisionOn( $dbw, $nullRevision ); 03916 03917 wfRunHooks( 'NewRevisionFromEditComplete', 03918 array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) ); 03919 03920 $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) ); 03921 03922 if ( !$moveOverRedirect ) { 03923 WikiPage::onArticleCreate( $nt ); 03924 } 03925 03926 # Recreate the redirect, this time in the other direction. 03927 if ( $redirectContent ) { 03928 $redirectArticle = WikiPage::factory( $this ); 03929 $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // bug 46397 03930 $newid = $redirectArticle->insertOn( $dbw ); 03931 if ( $newid ) { // sanity 03932 $this->resetArticleID( $newid ); 03933 $redirectRevision = new Revision( array( 03934 'title' => $this, // for determining the default content model 03935 'page' => $newid, 03936 'comment' => $comment, 03937 'content' => $redirectContent ) ); 03938 $redirectRevision->insertOn( $dbw ); 03939 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); 03940 03941 wfRunHooks( 'NewRevisionFromEditComplete', 03942 array( $redirectArticle, $redirectRevision, false, $wgUser ) ); 03943 03944 $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) ); 03945 } 03946 } 03947 03948 # Log the move 03949 $logid = $logEntry->insert(); 03950 $logEntry->publish( $logid ); 03951 } 03952 03965 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { 03966 global $wgMaximumMovedPages; 03967 // Check permissions 03968 if ( !$this->userCan( 'move-subpages' ) ) { 03969 return array( 'cant-move-subpages' ); 03970 } 03971 // Do the source and target namespaces support subpages? 03972 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03973 return array( 'namespace-nosubpages', 03974 MWNamespace::getCanonicalName( $this->getNamespace() ) ); 03975 } 03976 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) { 03977 return array( 'namespace-nosubpages', 03978 MWNamespace::getCanonicalName( $nt->getNamespace() ) ); 03979 } 03980 03981 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 ); 03982 $retval = array(); 03983 $count = 0; 03984 foreach ( $subpages as $oldSubpage ) { 03985 $count++; 03986 if ( $count > $wgMaximumMovedPages ) { 03987 $retval[$oldSubpage->getPrefixedTitle()] = 03988 array( 'movepage-max-pages', 03989 $wgMaximumMovedPages ); 03990 break; 03991 } 03992 03993 // We don't know whether this function was called before 03994 // or after moving the root page, so check both 03995 // $this and $nt 03996 if ( $oldSubpage->getArticleID() == $this->getArticleID() || 03997 $oldSubpage->getArticleID() == $nt->getArticleID() ) 03998 { 03999 // When moving a page to a subpage of itself, 04000 // don't move it twice 04001 continue; 04002 } 04003 $newPageName = preg_replace( 04004 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#', 04005 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 04006 $oldSubpage->getDBkey() ); 04007 if ( $oldSubpage->isTalkPage() ) { 04008 $newNs = $nt->getTalkPage()->getNamespace(); 04009 } else { 04010 $newNs = $nt->getSubjectPage()->getNamespace(); 04011 } 04012 # Bug 14385: we need makeTitleSafe because the new page names may 04013 # be longer than 255 characters. 04014 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); 04015 04016 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); 04017 if ( $success === true ) { 04018 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); 04019 } else { 04020 $retval[$oldSubpage->getPrefixedText()] = $success; 04021 } 04022 } 04023 return $retval; 04024 } 04025 04032 public function isSingleRevRedirect() { 04033 global $wgContentHandlerUseDB; 04034 04035 $dbw = wfGetDB( DB_MASTER ); 04036 04037 # Is it a redirect? 04038 $fields = array( 'page_is_redirect', 'page_latest', 'page_id' ); 04039 if ( $wgContentHandlerUseDB ) { 04040 $fields[] = 'page_content_model'; 04041 } 04042 04043 $row = $dbw->selectRow( 'page', 04044 $fields, 04045 $this->pageCond(), 04046 __METHOD__, 04047 array( 'FOR UPDATE' ) 04048 ); 04049 # Cache some fields we may want 04050 $this->mArticleID = $row ? intval( $row->page_id ) : 0; 04051 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; 04052 $this->mLatestID = $row ? intval( $row->page_latest ) : false; 04053 $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false; 04054 if ( !$this->mRedirect ) { 04055 return false; 04056 } 04057 # Does the article have a history? 04058 $row = $dbw->selectField( array( 'page', 'revision' ), 04059 'rev_id', 04060 array( 'page_namespace' => $this->getNamespace(), 04061 'page_title' => $this->getDBkey(), 04062 'page_id=rev_page', 04063 'page_latest != rev_id' 04064 ), 04065 __METHOD__, 04066 array( 'FOR UPDATE' ) 04067 ); 04068 # Return true if there was no history 04069 return ( $row === false ); 04070 } 04071 04079 public function isValidMoveTarget( $nt ) { 04080 # Is it an existing file? 04081 if ( $nt->getNamespace() == NS_FILE ) { 04082 $file = wfLocalFile( $nt ); 04083 if ( $file->exists() ) { 04084 wfDebug( __METHOD__ . ": file exists\n" ); 04085 return false; 04086 } 04087 } 04088 # Is it a redirect with no history? 04089 if ( !$nt->isSingleRevRedirect() ) { 04090 wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); 04091 return false; 04092 } 04093 # Get the article text 04094 $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST ); 04095 if ( !is_object( $rev ) ) { 04096 return false; 04097 } 04098 $content = $rev->getContent(); 04099 # Does the redirect point to the source? 04100 # Or is it a broken self-redirect, usually caused by namespace collisions? 04101 $redirTitle = $content ? $content->getRedirectTarget() : null; 04102 04103 if ( $redirTitle ) { 04104 if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && 04105 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) { 04106 wfDebug( __METHOD__ . ": redirect points to other page\n" ); 04107 return false; 04108 } else { 04109 return true; 04110 } 04111 } else { 04112 # Fail safe (not a redirect after all. strange.) 04113 wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . 04114 " is a redirect, but it doesn't contain a valid redirect.\n" ); 04115 return false; 04116 } 04117 } 04118 04126 public function getParentCategories() { 04127 global $wgContLang; 04128 04129 $data = array(); 04130 04131 $titleKey = $this->getArticleID(); 04132 04133 if ( $titleKey === 0 ) { 04134 return $data; 04135 } 04136 04137 $dbr = wfGetDB( DB_SLAVE ); 04138 04139 $res = $dbr->select( 04140 'categorylinks', 04141 'cl_to', 04142 array( 'cl_from' => $titleKey ), 04143 __METHOD__ 04144 ); 04145 04146 if ( $res->numRows() > 0 ) { 04147 foreach ( $res as $row ) { 04148 // $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to); 04149 $data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText(); 04150 } 04151 } 04152 return $data; 04153 } 04154 04161 public function getParentCategoryTree( $children = array() ) { 04162 $stack = array(); 04163 $parents = $this->getParentCategories(); 04164 04165 if ( $parents ) { 04166 foreach ( $parents as $parent => $current ) { 04167 if ( array_key_exists( $parent, $children ) ) { 04168 # Circular reference 04169 $stack[$parent] = array(); 04170 } else { 04171 $nt = Title::newFromText( $parent ); 04172 if ( $nt ) { 04173 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) ); 04174 } 04175 } 04176 } 04177 } 04178 04179 return $stack; 04180 } 04181 04188 public function pageCond() { 04189 if ( $this->mArticleID > 0 ) { 04190 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs 04191 return array( 'page_id' => $this->mArticleID ); 04192 } else { 04193 return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); 04194 } 04195 } 04196 04204 public function getPreviousRevisionID( $revId, $flags = 0 ) { 04205 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04206 $revId = $db->selectField( 'revision', 'rev_id', 04207 array( 04208 'rev_page' => $this->getArticleID( $flags ), 04209 'rev_id < ' . intval( $revId ) 04210 ), 04211 __METHOD__, 04212 array( 'ORDER BY' => 'rev_id DESC' ) 04213 ); 04214 04215 if ( $revId === false ) { 04216 return false; 04217 } else { 04218 return intval( $revId ); 04219 } 04220 } 04221 04229 public function getNextRevisionID( $revId, $flags = 0 ) { 04230 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04231 $revId = $db->selectField( 'revision', 'rev_id', 04232 array( 04233 'rev_page' => $this->getArticleID( $flags ), 04234 'rev_id > ' . intval( $revId ) 04235 ), 04236 __METHOD__, 04237 array( 'ORDER BY' => 'rev_id' ) 04238 ); 04239 04240 if ( $revId === false ) { 04241 return false; 04242 } else { 04243 return intval( $revId ); 04244 } 04245 } 04246 04253 public function getFirstRevision( $flags = 0 ) { 04254 $pageId = $this->getArticleID( $flags ); 04255 if ( $pageId ) { 04256 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04257 $row = $db->selectRow( 'revision', Revision::selectFields(), 04258 array( 'rev_page' => $pageId ), 04259 __METHOD__, 04260 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) 04261 ); 04262 if ( $row ) { 04263 return new Revision( $row ); 04264 } 04265 } 04266 return null; 04267 } 04268 04275 public function getEarliestRevTime( $flags = 0 ) { 04276 $rev = $this->getFirstRevision( $flags ); 04277 return $rev ? $rev->getTimestamp() : null; 04278 } 04279 04285 public function isNewPage() { 04286 $dbr = wfGetDB( DB_SLAVE ); 04287 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); 04288 } 04289 04295 public function isBigDeletion() { 04296 global $wgDeleteRevisionsLimit; 04297 04298 if ( !$wgDeleteRevisionsLimit ) { 04299 return false; 04300 } 04301 04302 $revCount = $this->estimateRevisionCount(); 04303 return $revCount > $wgDeleteRevisionsLimit; 04304 } 04305 04311 public function estimateRevisionCount() { 04312 if ( !$this->exists() ) { 04313 return 0; 04314 } 04315 04316 if ( $this->mEstimateRevisions === null ) { 04317 $dbr = wfGetDB( DB_SLAVE ); 04318 $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', 04319 array( 'rev_page' => $this->getArticleID() ), __METHOD__ ); 04320 } 04321 04322 return $this->mEstimateRevisions; 04323 } 04324 04333 public function countRevisionsBetween( $old, $new ) { 04334 if ( !( $old instanceof Revision ) ) { 04335 $old = Revision::newFromTitle( $this, (int)$old ); 04336 } 04337 if ( !( $new instanceof Revision ) ) { 04338 $new = Revision::newFromTitle( $this, (int)$new ); 04339 } 04340 if ( !$old || !$new ) { 04341 return 0; // nothing to compare 04342 } 04343 $dbr = wfGetDB( DB_SLAVE ); 04344 return (int)$dbr->selectField( 'revision', 'count(*)', 04345 array( 04346 'rev_page' => $this->getArticleID(), 04347 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04348 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04349 ), 04350 __METHOD__ 04351 ); 04352 } 04353 04368 public function countAuthorsBetween( $old, $new, $limit, $options = array() ) { 04369 if ( !( $old instanceof Revision ) ) { 04370 $old = Revision::newFromTitle( $this, (int)$old ); 04371 } 04372 if ( !( $new instanceof Revision ) ) { 04373 $new = Revision::newFromTitle( $this, (int)$new ); 04374 } 04375 // XXX: what if Revision objects are passed in, but they don't refer to this title? 04376 // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID() 04377 // in the sanity check below? 04378 if ( !$old || !$new ) { 04379 return 0; // nothing to compare 04380 } 04381 $old_cmp = '>'; 04382 $new_cmp = '<'; 04383 $options = (array)$options; 04384 if ( in_array( 'include_old', $options ) ) { 04385 $old_cmp = '>='; 04386 } 04387 if ( in_array( 'include_new', $options ) ) { 04388 $new_cmp = '<='; 04389 } 04390 if ( in_array( 'include_both', $options ) ) { 04391 $old_cmp = '>='; 04392 $new_cmp = '<='; 04393 } 04394 // No DB query needed if $old and $new are the same or successive revisions: 04395 if ( $old->getId() === $new->getId() ) { 04396 return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; 04397 } elseif ( $old->getId() === $new->getParentId() ) { 04398 if ( $old_cmp === '>' || $new_cmp === '<' ) { 04399 return ( $old_cmp === '>' && $new_cmp === '<' ) ? 0 : 1; 04400 } 04401 return ( $old->getRawUserText() === $new->getRawUserText() ) ? 1 : 2; 04402 } 04403 $dbr = wfGetDB( DB_SLAVE ); 04404 $res = $dbr->select( 'revision', 'DISTINCT rev_user_text', 04405 array( 04406 'rev_page' => $this->getArticleID(), 04407 "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04408 "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04409 ), __METHOD__, 04410 array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated 04411 ); 04412 return (int)$dbr->numRows( $res ); 04413 } 04414 04421 public function equals( Title $title ) { 04422 // Note: === is necessary for proper matching of number-like titles. 04423 return $this->getInterwiki() === $title->getInterwiki() 04424 && $this->getNamespace() == $title->getNamespace() 04425 && $this->getDBkey() === $title->getDBkey(); 04426 } 04427 04434 public function isSubpageOf( Title $title ) { 04435 return $this->getInterwiki() === $title->getInterwiki() 04436 && $this->getNamespace() == $title->getNamespace() 04437 && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; 04438 } 04439 04449 public function exists() { 04450 return $this->getArticleID() != 0; 04451 } 04452 04469 public function isAlwaysKnown() { 04470 $isKnown = null; 04471 04482 wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) ); 04483 04484 if ( !is_null( $isKnown ) ) { 04485 return $isKnown; 04486 } 04487 04488 if ( $this->mInterwiki != '' ) { 04489 return true; // any interwiki link might be viewable, for all we know 04490 } 04491 04492 switch ( $this->mNamespace ) { 04493 case NS_MEDIA: 04494 case NS_FILE: 04495 // file exists, possibly in a foreign repo 04496 return (bool)wfFindFile( $this ); 04497 case NS_SPECIAL: 04498 // valid special page 04499 return SpecialPageFactory::exists( $this->getDBkey() ); 04500 case NS_MAIN: 04501 // selflink, possibly with fragment 04502 return $this->mDbkeyform == ''; 04503 case NS_MEDIAWIKI: 04504 // known system message 04505 return $this->hasSourceText() !== false; 04506 default: 04507 return false; 04508 } 04509 } 04510 04522 public function isKnown() { 04523 return $this->isAlwaysKnown() || $this->exists(); 04524 } 04525 04531 public function hasSourceText() { 04532 if ( $this->exists() ) { 04533 return true; 04534 } 04535 04536 if ( $this->mNamespace == NS_MEDIAWIKI ) { 04537 // If the page doesn't exist but is a known system message, default 04538 // message content will be displayed, same for language subpages- 04539 // Use always content language to avoid loading hundreds of languages 04540 // to get the link color. 04541 global $wgContLang; 04542 list( $name, ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04543 $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false ); 04544 return $message->exists(); 04545 } 04546 04547 return false; 04548 } 04549 04555 public function getDefaultMessageText() { 04556 global $wgContLang; 04557 04558 if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case 04559 return false; 04560 } 04561 04562 list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04563 $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false ); 04564 04565 if ( $message->exists() ) { 04566 return $message->plain(); 04567 } else { 04568 return false; 04569 } 04570 } 04571 04577 public function invalidateCache() { 04578 if ( wfReadOnly() ) { 04579 return false; 04580 } 04581 04582 $method = __METHOD__; 04583 $dbw = wfGetDB( DB_MASTER ); 04584 $conds = $this->pageCond(); 04585 $dbw->onTransactionIdle( function() use ( $dbw, $conds, $method ) { 04586 $dbw->update( 04587 'page', 04588 array( 'page_touched' => $dbw->timestamp() ), 04589 $conds, 04590 $method 04591 ); 04592 } ); 04593 04594 return true; 04595 } 04596 04602 public function touchLinks() { 04603 $u = new HTMLCacheUpdate( $this, 'pagelinks' ); 04604 $u->doUpdate(); 04605 04606 if ( $this->getNamespace() == NS_CATEGORY ) { 04607 $u = new HTMLCacheUpdate( $this, 'categorylinks' ); 04608 $u->doUpdate(); 04609 } 04610 } 04611 04618 public function getTouched( $db = null ) { 04619 $db = isset( $db ) ? $db : wfGetDB( DB_SLAVE ); 04620 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); 04621 return $touched; 04622 } 04623 04630 public function getNotificationTimestamp( $user = null ) { 04631 global $wgUser, $wgShowUpdatedMarker; 04632 // Assume current user if none given 04633 if ( !$user ) { 04634 $user = $wgUser; 04635 } 04636 // Check cache first 04637 $uid = $user->getId(); 04638 // avoid isset here, as it'll return false for null entries 04639 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) { 04640 return $this->mNotificationTimestamp[$uid]; 04641 } 04642 if ( !$uid || !$wgShowUpdatedMarker || !$user->isAllowed( 'viewmywatchlist' ) ) { 04643 return $this->mNotificationTimestamp[$uid] = false; 04644 } 04645 // Don't cache too much! 04646 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) { 04647 $this->mNotificationTimestamp = array(); 04648 } 04649 $dbr = wfGetDB( DB_SLAVE ); 04650 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', 04651 'wl_notificationtimestamp', 04652 array( 04653 'wl_user' => $user->getId(), 04654 'wl_namespace' => $this->getNamespace(), 04655 'wl_title' => $this->getDBkey(), 04656 ), 04657 __METHOD__ 04658 ); 04659 return $this->mNotificationTimestamp[$uid]; 04660 } 04661 04668 public function getNamespaceKey( $prepend = 'nstab-' ) { 04669 global $wgContLang; 04670 // Gets the subject namespace if this title 04671 $namespace = MWNamespace::getSubject( $this->getNamespace() ); 04672 // Checks if canonical namespace name exists for namespace 04673 if ( MWNamespace::exists( $this->getNamespace() ) ) { 04674 // Uses canonical namespace name 04675 $namespaceKey = MWNamespace::getCanonicalName( $namespace ); 04676 } else { 04677 // Uses text of namespace 04678 $namespaceKey = $this->getSubjectNsText(); 04679 } 04680 // Makes namespace key lowercase 04681 $namespaceKey = $wgContLang->lc( $namespaceKey ); 04682 // Uses main 04683 if ( $namespaceKey == '' ) { 04684 $namespaceKey = 'main'; 04685 } 04686 // Changes file to image for backwards compatibility 04687 if ( $namespaceKey == 'file' ) { 04688 $namespaceKey = 'image'; 04689 } 04690 return $prepend . $namespaceKey; 04691 } 04692 04699 public function getRedirectsHere( $ns = null ) { 04700 $redirs = array(); 04701 04702 $dbr = wfGetDB( DB_SLAVE ); 04703 $where = array( 04704 'rd_namespace' => $this->getNamespace(), 04705 'rd_title' => $this->getDBkey(), 04706 'rd_from = page_id' 04707 ); 04708 if ( $this->isExternal() ) { 04709 $where['rd_interwiki'] = $this->getInterwiki(); 04710 } else { 04711 $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'; 04712 } 04713 if ( !is_null( $ns ) ) { 04714 $where['page_namespace'] = $ns; 04715 } 04716 04717 $res = $dbr->select( 04718 array( 'redirect', 'page' ), 04719 array( 'page_namespace', 'page_title' ), 04720 $where, 04721 __METHOD__ 04722 ); 04723 04724 foreach ( $res as $row ) { 04725 $redirs[] = self::newFromRow( $row ); 04726 } 04727 return $redirs; 04728 } 04729 04735 public function isValidRedirectTarget() { 04736 global $wgInvalidRedirectTargets; 04737 04738 // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here 04739 if ( $this->isSpecial( 'Userlogout' ) ) { 04740 return false; 04741 } 04742 04743 foreach ( $wgInvalidRedirectTargets as $target ) { 04744 if ( $this->isSpecial( $target ) ) { 04745 return false; 04746 } 04747 } 04748 04749 return true; 04750 } 04751 04757 public function getBacklinkCache() { 04758 return BacklinkCache::get( $this ); 04759 } 04760 04766 public function canUseNoindex() { 04767 global $wgContentNamespaces, $wgExemptFromUserRobotsControl; 04768 04769 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl ) 04770 ? $wgContentNamespaces 04771 : $wgExemptFromUserRobotsControl; 04772 04773 return !in_array( $this->mNamespace, $bannedNamespaces ); 04774 04775 } 04776 04787 public function getCategorySortkey( $prefix = '' ) { 04788 $unprefixed = $this->getText(); 04789 04790 // Anything that uses this hook should only depend 04791 // on the Title object passed in, and should probably 04792 // tell the users to run updateCollations.php --force 04793 // in order to re-sort existing category relations. 04794 wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) ); 04795 if ( $prefix !== '' ) { 04796 # Separate with a line feed, so the unprefixed part is only used as 04797 # a tiebreaker when two pages have the exact same prefix. 04798 # In UCA, tab is the only character that can sort above LF 04799 # so we strip both of them from the original prefix. 04800 $prefix = strtr( $prefix, "\n\t", ' ' ); 04801 return "$prefix\n$unprefixed"; 04802 } 04803 return $unprefixed; 04804 } 04805 04814 public function getPageLanguage() { 04815 global $wgLang; 04816 if ( $this->isSpecialPage() ) { 04817 // special pages are in the user language 04818 return $wgLang; 04819 } 04820 04821 //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request. 04822 //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language! 04823 $contentHandler = ContentHandler::getForTitle( $this ); 04824 $pageLang = $contentHandler->getPageLanguage( $this ); 04825 04826 return wfGetLangObj( $pageLang ); 04827 } 04828 04837 public function getPageViewLanguage() { 04838 global $wgLang; 04839 04840 if ( $this->isSpecialPage() ) { 04841 // If the user chooses a variant, the content is actually 04842 // in a language whose code is the variant code. 04843 $variant = $wgLang->getPreferredVariant(); 04844 if ( $wgLang->getCode() !== $variant ) { 04845 return Language::factory( $variant ); 04846 } 04847 04848 return $wgLang; 04849 } 04850 04851 //NOTE: can't be cached persistently, depends on user settings 04852 //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language! 04853 $contentHandler = ContentHandler::getForTitle( $this ); 04854 $pageLang = $contentHandler->getPageViewLanguage( $this ); 04855 return $pageLang; 04856 } 04857 04868 public function getEditNotices( $oldid = 0 ) { 04869 $notices = array(); 04870 04871 # Optional notices on a per-namespace and per-page basis 04872 $editnotice_ns = 'editnotice-' . $this->getNamespace(); 04873 $editnotice_ns_message = wfMessage( $editnotice_ns ); 04874 if ( $editnotice_ns_message->exists() ) { 04875 $notices[$editnotice_ns] = $editnotice_ns_message->parseAsBlock(); 04876 } 04877 if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) { 04878 $parts = explode( '/', $this->getDBkey() ); 04879 $editnotice_base = $editnotice_ns; 04880 while ( count( $parts ) > 0 ) { 04881 $editnotice_base .= '-' . array_shift( $parts ); 04882 $editnotice_base_msg = wfMessage( $editnotice_base ); 04883 if ( $editnotice_base_msg->exists() ) { 04884 $notices[$editnotice_base] = $editnotice_base_msg->parseAsBlock(); 04885 } 04886 } 04887 } else { 04888 # Even if there are no subpages in namespace, we still don't want / in MW ns. 04889 $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() ); 04890 $editnoticeMsg = wfMessage( $editnoticeText ); 04891 if ( $editnoticeMsg->exists() ) { 04892 $notices[$editnoticeText] = $editnoticeMsg->parseAsBlock(); 04893 } 04894 } 04895 04896 wfRunHooks( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) ); 04897 return $notices; 04898 } 04899 }