MediaWiki
REL1_23
|
00001 <?php 00035 class Title { 00037 static private $titleCache = null; 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 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 = null; 00079 var $mTitleProtection; 00080 # Don't change the following default, NS_MAIN is hardcoded in several 00081 # places. See bug 696. 00082 # Zero except in {{transclusion}} tags 00083 var $mDefaultNamespace = NS_MAIN; // /< Namespace index when there is no namespace 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 private $mPageLanguage = false; // /< The (string) language code of the page's language and content code. 00090 private $mTitleValue = null; // /< A corresponding TitleValue object 00091 // @} 00092 00101 private static function getTitleParser() { 00102 global $wgContLang, $wgLocalInterwikis; 00103 00104 static $titleCodec = null; 00105 static $titleCodecFingerprint = null; 00106 00107 // $wgContLang and $wgLocalInterwikis may change (especially while testing), 00108 // make sure we are using the right one. To detect changes over the course 00109 // of a request, we remember a fingerprint of the config used to create the 00110 // codec singleton, and re-create it if the fingerprint doesn't match. 00111 $fingerprint = spl_object_hash( $wgContLang ) . '|' . join( '+', $wgLocalInterwikis ); 00112 00113 if ( $fingerprint !== $titleCodecFingerprint ) { 00114 $titleCodec = null; 00115 } 00116 00117 if ( !$titleCodec ) { 00118 $titleCodec = new MediaWikiTitleCodec( $wgContLang, GenderCache::singleton(), $wgLocalInterwikis ); 00119 $titleCodecFingerprint = $fingerprint; 00120 } 00121 00122 return $titleCodec; 00123 } 00124 00133 private static function getTitleFormatter() { 00134 //NOTE: we know that getTitleParser() returns a MediaWikiTitleCodec, 00135 // which implements TitleFormatter. 00136 return self::getTitleParser(); 00137 } 00138 00142 /*protected*/ function __construct() { } 00143 00152 public static function newFromDBkey( $key ) { 00153 $t = new Title(); 00154 $t->mDbkeyform = $key; 00155 if ( $t->secureAndSplit() ) { 00156 return $t; 00157 } else { 00158 return null; 00159 } 00160 } 00161 00169 public static function newFromTitleValue( TitleValue $titleValue ) { 00170 return self::makeTitle( 00171 $titleValue->getNamespace(), 00172 $titleValue->getText(), 00173 $titleValue->getFragment() ); 00174 } 00175 00189 public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { 00190 if ( is_object( $text ) ) { 00191 throw new MWException( 'Title::newFromText given an object' ); 00192 } 00193 00194 $cache = self::getTitleCache(); 00195 00204 if ( $defaultNamespace == NS_MAIN && $cache->has( $text ) ) { 00205 return $cache->get( $text ); 00206 } 00207 00208 # Convert things like é ā or 〗 into normalized (bug 14952) text 00209 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); 00210 00211 $t = new Title(); 00212 $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); 00213 $t->mDefaultNamespace = intval( $defaultNamespace ); 00214 00215 if ( $t->secureAndSplit() ) { 00216 if ( $defaultNamespace == NS_MAIN ) { 00217 $cache->set( $text, $t ); 00218 } 00219 return $t; 00220 } else { 00221 $ret = null; 00222 return $ret; 00223 } 00224 } 00225 00241 public static function newFromURL( $url ) { 00242 $t = new Title(); 00243 00244 # For compatibility with old buggy URLs. "+" is usually not valid in titles, 00245 # but some URLs used it as a space replacement and they still come 00246 # from some external search tools. 00247 if ( strpos( self::legalChars(), '+' ) === false ) { 00248 $url = str_replace( '+', ' ', $url ); 00249 } 00250 00251 $t->mDbkeyform = str_replace( ' ', '_', $url ); 00252 if ( $t->secureAndSplit() ) { 00253 return $t; 00254 } else { 00255 return null; 00256 } 00257 } 00258 00262 private static function getTitleCache() { 00263 if ( self::$titleCache == null ) { 00264 self::$titleCache = new MapCacheLRU( self::CACHE_MAX ); 00265 } 00266 return self::$titleCache; 00267 } 00268 00275 protected static function getSelectFields() { 00276 global $wgContentHandlerUseDB; 00277 00278 $fields = array( 00279 'page_namespace', 'page_title', 'page_id', 00280 'page_len', 'page_is_redirect', 'page_latest', 00281 ); 00282 00283 if ( $wgContentHandlerUseDB ) { 00284 $fields[] = 'page_content_model'; 00285 } 00286 00287 return $fields; 00288 } 00289 00297 public static function newFromID( $id, $flags = 0 ) { 00298 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 00299 $row = $db->selectRow( 00300 'page', 00301 self::getSelectFields(), 00302 array( 'page_id' => $id ), 00303 __METHOD__ 00304 ); 00305 if ( $row !== false ) { 00306 $title = Title::newFromRow( $row ); 00307 } else { 00308 $title = null; 00309 } 00310 return $title; 00311 } 00312 00319 public static function newFromIDs( $ids ) { 00320 if ( !count( $ids ) ) { 00321 return array(); 00322 } 00323 $dbr = wfGetDB( DB_SLAVE ); 00324 00325 $res = $dbr->select( 00326 'page', 00327 self::getSelectFields(), 00328 array( 'page_id' => $ids ), 00329 __METHOD__ 00330 ); 00331 00332 $titles = array(); 00333 foreach ( $res as $row ) { 00334 $titles[] = Title::newFromRow( $row ); 00335 } 00336 return $titles; 00337 } 00338 00345 public static function newFromRow( $row ) { 00346 $t = self::makeTitle( $row->page_namespace, $row->page_title ); 00347 $t->loadFromRow( $row ); 00348 return $t; 00349 } 00350 00357 public function loadFromRow( $row ) { 00358 if ( $row ) { // page found 00359 if ( isset( $row->page_id ) ) { 00360 $this->mArticleID = (int)$row->page_id; 00361 } 00362 if ( isset( $row->page_len ) ) { 00363 $this->mLength = (int)$row->page_len; 00364 } 00365 if ( isset( $row->page_is_redirect ) ) { 00366 $this->mRedirect = (bool)$row->page_is_redirect; 00367 } 00368 if ( isset( $row->page_latest ) ) { 00369 $this->mLatestID = (int)$row->page_latest; 00370 } 00371 if ( isset( $row->page_content_model ) ) { 00372 $this->mContentModel = strval( $row->page_content_model ); 00373 } else { 00374 $this->mContentModel = false; # initialized lazily in getContentModel() 00375 } 00376 } else { // page not found 00377 $this->mArticleID = 0; 00378 $this->mLength = 0; 00379 $this->mRedirect = false; 00380 $this->mLatestID = 0; 00381 $this->mContentModel = false; # initialized lazily in getContentModel() 00382 } 00383 } 00384 00398 public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { 00399 $t = new Title(); 00400 $t->mInterwiki = $interwiki; 00401 $t->mFragment = $fragment; 00402 $t->mNamespace = $ns = intval( $ns ); 00403 $t->mDbkeyform = str_replace( ' ', '_', $title ); 00404 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; 00405 $t->mUrlform = wfUrlencode( $t->mDbkeyform ); 00406 $t->mTextform = str_replace( '_', ' ', $title ); 00407 $t->mContentModel = false; # initialized lazily in getContentModel() 00408 return $t; 00409 } 00410 00422 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { 00423 if ( !MWNamespace::exists( $ns ) ) { 00424 return null; 00425 } 00426 00427 $t = new Title(); 00428 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki ); 00429 if ( $t->secureAndSplit() ) { 00430 return $t; 00431 } else { 00432 return null; 00433 } 00434 } 00435 00441 public static function newMainPage() { 00442 $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() ); 00443 // Don't give fatal errors if the message is broken 00444 if ( !$title ) { 00445 $title = Title::newFromText( 'Main Page' ); 00446 } 00447 return $title; 00448 } 00449 00460 public static function newFromRedirect( $text ) { 00461 ContentHandler::deprecated( __METHOD__, '1.21' ); 00462 00463 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00464 return $content->getRedirectTarget(); 00465 } 00466 00477 public static function newFromRedirectRecurse( $text ) { 00478 ContentHandler::deprecated( __METHOD__, '1.21' ); 00479 00480 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00481 return $content->getUltimateRedirectTarget(); 00482 } 00483 00494 public static function newFromRedirectArray( $text ) { 00495 ContentHandler::deprecated( __METHOD__, '1.21' ); 00496 00497 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00498 return $content->getRedirectChain(); 00499 } 00500 00507 public static function nameOf( $id ) { 00508 $dbr = wfGetDB( DB_SLAVE ); 00509 00510 $s = $dbr->selectRow( 00511 'page', 00512 array( 'page_namespace', 'page_title' ), 00513 array( 'page_id' => $id ), 00514 __METHOD__ 00515 ); 00516 if ( $s === false ) { 00517 return null; 00518 } 00519 00520 $n = self::makeName( $s->page_namespace, $s->page_title ); 00521 return $n; 00522 } 00523 00529 public static function legalChars() { 00530 global $wgLegalTitleChars; 00531 return $wgLegalTitleChars; 00532 } 00533 00543 static function getTitleInvalidRegex() { 00544 static $rxTc = false; 00545 if ( !$rxTc ) { 00546 # Matching titles will be held as illegal. 00547 $rxTc = '/' . 00548 # Any character not allowed is forbidden... 00549 '[^' . self::legalChars() . ']' . 00550 # URL percent encoding sequences interfere with the ability 00551 # to round-trip titles -- you can't link to them consistently. 00552 '|%[0-9A-Fa-f]{2}' . 00553 # XML/HTML character references produce similar issues. 00554 '|&[A-Za-z0-9\x80-\xff]+;' . 00555 '|&#[0-9]+;' . 00556 '|&#x[0-9A-Fa-f]+;' . 00557 '/S'; 00558 } 00559 00560 return $rxTc; 00561 } 00562 00572 public static function convertByteClassToUnicodeClass( $byteClass ) { 00573 $length = strlen( $byteClass ); 00574 // Input token queue 00575 $x0 = $x1 = $x2 = ''; 00576 // Decoded queue 00577 $d0 = $d1 = $d2 = ''; 00578 // Decoded integer codepoints 00579 $ord0 = $ord1 = $ord2 = 0; 00580 // Re-encoded queue 00581 $r0 = $r1 = $r2 = ''; 00582 // Output 00583 $out = ''; 00584 // Flags 00585 $allowUnicode = false; 00586 for ( $pos = 0; $pos < $length; $pos++ ) { 00587 // Shift the queues down 00588 $x2 = $x1; 00589 $x1 = $x0; 00590 $d2 = $d1; 00591 $d1 = $d0; 00592 $ord2 = $ord1; 00593 $ord1 = $ord0; 00594 $r2 = $r1; 00595 $r1 = $r0; 00596 // Load the current input token and decoded values 00597 $inChar = $byteClass[$pos]; 00598 if ( $inChar == '\\' ) { 00599 if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) { 00600 $x0 = $inChar . $m[0]; 00601 $d0 = chr( hexdec( $m[1] ) ); 00602 $pos += strlen( $m[0] ); 00603 } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) { 00604 $x0 = $inChar . $m[0]; 00605 $d0 = chr( octdec( $m[0] ) ); 00606 $pos += strlen( $m[0] ); 00607 } elseif ( $pos + 1 >= $length ) { 00608 $x0 = $d0 = '\\'; 00609 } else { 00610 $d0 = $byteClass[$pos + 1]; 00611 $x0 = $inChar . $d0; 00612 $pos += 1; 00613 } 00614 } else { 00615 $x0 = $d0 = $inChar; 00616 } 00617 $ord0 = ord( $d0 ); 00618 // Load the current re-encoded value 00619 if ( $ord0 < 32 || $ord0 == 0x7f ) { 00620 $r0 = sprintf( '\x%02x', $ord0 ); 00621 } elseif ( $ord0 >= 0x80 ) { 00622 // Allow unicode if a single high-bit character appears 00623 $r0 = sprintf( '\x%02x', $ord0 ); 00624 $allowUnicode = true; 00625 } elseif ( strpos( '-\\[]^', $d0 ) !== false ) { 00626 $r0 = '\\' . $d0; 00627 } else { 00628 $r0 = $d0; 00629 } 00630 // Do the output 00631 if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) { 00632 // Range 00633 if ( $ord2 > $ord0 ) { 00634 // Empty range 00635 } elseif ( $ord0 >= 0x80 ) { 00636 // Unicode range 00637 $allowUnicode = true; 00638 if ( $ord2 < 0x80 ) { 00639 // Keep the non-unicode section of the range 00640 $out .= "$r2-\\x7F"; 00641 } 00642 } else { 00643 // Normal range 00644 $out .= "$r2-$r0"; 00645 } 00646 // Reset state to the initial value 00647 $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = ''; 00648 } elseif ( $ord2 < 0x80 ) { 00649 // ASCII character 00650 $out .= $r2; 00651 } 00652 } 00653 if ( $ord1 < 0x80 ) { 00654 $out .= $r1; 00655 } 00656 if ( $ord0 < 0x80 ) { 00657 $out .= $r0; 00658 } 00659 if ( $allowUnicode ) { 00660 $out .= '\u0080-\uFFFF'; 00661 } 00662 return $out; 00663 } 00664 00673 public static function indexTitle( $ns, $title ) { 00674 global $wgContLang; 00675 00676 $lc = SearchEngine::legalSearchChars() . '&#;'; 00677 $t = $wgContLang->normalizeForSearch( $title ); 00678 $t = preg_replace( "/[^{$lc}]+/", ' ', $t ); 00679 $t = $wgContLang->lc( $t ); 00680 00681 # Handle 's, s' 00682 $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t ); 00683 $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t ); 00684 00685 $t = preg_replace( "/\\s+/", ' ', $t ); 00686 00687 if ( $ns == NS_FILE ) { 00688 $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t ); 00689 } 00690 return trim( $t ); 00691 } 00692 00702 public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) { 00703 global $wgContLang; 00704 00705 $namespace = $wgContLang->getNsText( $ns ); 00706 $name = $namespace == '' ? $title : "$namespace:$title"; 00707 if ( strval( $interwiki ) != '' ) { 00708 $name = "$interwiki:$name"; 00709 } 00710 if ( strval( $fragment ) != '' ) { 00711 $name .= '#' . $fragment; 00712 } 00713 return $name; 00714 } 00715 00722 static function escapeFragmentForURL( $fragment ) { 00723 # Note that we don't urlencode the fragment. urlencoded Unicode 00724 # fragments appear not to work in IE (at least up to 7) or in at least 00725 # one version of Opera 9.x. The W3C validator, for one, doesn't seem 00726 # to care if they aren't encoded. 00727 return Sanitizer::escapeId( $fragment, 'noninitial' ); 00728 } 00729 00738 public static function compare( $a, $b ) { 00739 if ( $a->getNamespace() == $b->getNamespace() ) { 00740 return strcmp( $a->getText(), $b->getText() ); 00741 } else { 00742 return $a->getNamespace() - $b->getNamespace(); 00743 } 00744 } 00745 00752 public function isLocal() { 00753 if ( $this->isExternal() ) { 00754 $iw = Interwiki::fetch( $this->mInterwiki ); 00755 if ( $iw ) { 00756 return $iw->isLocal(); 00757 } 00758 } 00759 return true; 00760 } 00761 00767 public function isExternal() { 00768 return $this->mInterwiki !== ''; 00769 } 00770 00778 public function getInterwiki() { 00779 return $this->mInterwiki; 00780 } 00781 00788 public function isTrans() { 00789 if ( !$this->isExternal() ) { 00790 return false; 00791 } 00792 00793 return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); 00794 } 00795 00801 public function getTransWikiID() { 00802 if ( !$this->isExternal() ) { 00803 return false; 00804 } 00805 00806 return Interwiki::fetch( $this->mInterwiki )->getWikiID(); 00807 } 00808 00818 public function getTitleValue() { 00819 if ( $this->mTitleValue === null ) { 00820 try { 00821 $this->mTitleValue = new TitleValue( 00822 $this->getNamespace(), 00823 $this->getDBkey(), 00824 $this->getFragment() ); 00825 } catch ( InvalidArgumentException $ex ) { 00826 wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' . 00827 $this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" ); 00828 } 00829 } 00830 00831 return $this->mTitleValue; 00832 } 00833 00839 public function getText() { 00840 return $this->mTextform; 00841 } 00842 00848 public function getPartialURL() { 00849 return $this->mUrlform; 00850 } 00851 00857 public function getDBkey() { 00858 return $this->mDbkeyform; 00859 } 00860 00866 function getUserCaseDBKey() { 00867 if ( !is_null( $this->mUserCaseDBKey ) ) { 00868 return $this->mUserCaseDBKey; 00869 } else { 00870 // If created via makeTitle(), $this->mUserCaseDBKey is not set. 00871 return $this->mDbkeyform; 00872 } 00873 } 00874 00880 public function getNamespace() { 00881 return $this->mNamespace; 00882 } 00883 00890 public function getContentModel() { 00891 if ( !$this->mContentModel ) { 00892 $linkCache = LinkCache::singleton(); 00893 $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); 00894 } 00895 00896 if ( !$this->mContentModel ) { 00897 $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); 00898 } 00899 00900 if ( !$this->mContentModel ) { 00901 throw new MWException( 'Failed to determine content model!' ); 00902 } 00903 00904 return $this->mContentModel; 00905 } 00906 00913 public function hasContentModel( $id ) { 00914 return $this->getContentModel() == $id; 00915 } 00916 00922 public function getNsText() { 00923 if ( $this->isExternal() ) { 00924 // This probably shouldn't even happen. ohh man, oh yuck. 00925 // But for interwiki transclusion it sometimes does. 00926 // Shit. Shit shit shit. 00927 // 00928 // Use the canonical namespaces if possible to try to 00929 // resolve a foreign namespace. 00930 if ( MWNamespace::exists( $this->mNamespace ) ) { 00931 return MWNamespace::getCanonicalName( $this->mNamespace ); 00932 } 00933 } 00934 00935 try { 00936 $formatter = $this->getTitleFormatter(); 00937 return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform ); 00938 } catch ( InvalidArgumentException $ex ) { 00939 wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" ); 00940 return false; 00941 } 00942 } 00943 00949 public function getSubjectNsText() { 00950 global $wgContLang; 00951 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) ); 00952 } 00953 00959 public function getTalkNsText() { 00960 global $wgContLang; 00961 return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ); 00962 } 00963 00969 public function canTalk() { 00970 return MWNamespace::canTalk( $this->mNamespace ); 00971 } 00972 00979 public function canExist() { 00980 return $this->mNamespace >= NS_MAIN; 00981 } 00982 00988 public function isWatchable() { 00989 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); 00990 } 00991 00997 public function isSpecialPage() { 00998 return $this->getNamespace() == NS_SPECIAL; 00999 } 01000 01007 public function isSpecial( $name ) { 01008 if ( $this->isSpecialPage() ) { 01009 list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); 01010 if ( $name == $thisName ) { 01011 return true; 01012 } 01013 } 01014 return false; 01015 } 01016 01023 public function fixSpecialName() { 01024 if ( $this->isSpecialPage() ) { 01025 list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); 01026 if ( $canonicalName ) { 01027 $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); 01028 if ( $localName != $this->mDbkeyform ) { 01029 return Title::makeTitle( NS_SPECIAL, $localName ); 01030 } 01031 } 01032 } 01033 return $this; 01034 } 01035 01046 public function inNamespace( $ns ) { 01047 return MWNamespace::equals( $this->getNamespace(), $ns ); 01048 } 01049 01057 public function inNamespaces( /* ... */ ) { 01058 $namespaces = func_get_args(); 01059 if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { 01060 $namespaces = $namespaces[0]; 01061 } 01062 01063 foreach ( $namespaces as $ns ) { 01064 if ( $this->inNamespace( $ns ) ) { 01065 return true; 01066 } 01067 } 01068 01069 return false; 01070 } 01071 01085 public function hasSubjectNamespace( $ns ) { 01086 return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); 01087 } 01088 01096 public function isContentPage() { 01097 return MWNamespace::isContent( $this->getNamespace() ); 01098 } 01099 01106 public function isMovable() { 01107 if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) { 01108 // Interwiki title or immovable namespace. Hooks don't get to override here 01109 return false; 01110 } 01111 01112 $result = true; 01113 wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); 01114 return $result; 01115 } 01116 01127 public function isMainPage() { 01128 return $this->equals( Title::newMainPage() ); 01129 } 01130 01136 public function isSubpage() { 01137 return MWNamespace::hasSubpages( $this->mNamespace ) 01138 ? strpos( $this->getText(), '/' ) !== false 01139 : false; 01140 } 01141 01147 public function isConversionTable() { 01148 // @todo ConversionTable should become a separate content model. 01149 01150 return $this->getNamespace() == NS_MEDIAWIKI && 01151 strpos( $this->getText(), 'Conversiontable/' ) === 0; 01152 } 01153 01159 public function isWikitextPage() { 01160 return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); 01161 } 01162 01174 public function isCssOrJsPage() { 01175 $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace 01176 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 01177 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01178 01179 #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure 01180 # hook functions can force this method to return true even outside the mediawiki namespace. 01181 01182 wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) ); 01183 01184 return $isCssOrJsPage; 01185 } 01186 01191 public function isCssJsSubpage() { 01192 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01193 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 01194 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); 01195 } 01196 01202 public function getSkinFromCssJsSubpage() { 01203 $subpage = explode( '/', $this->mTextform ); 01204 $subpage = $subpage[count( $subpage ) - 1]; 01205 $lastdot = strrpos( $subpage, '.' ); 01206 if ( $lastdot === false ) { 01207 return $subpage; # Never happens: only called for names ending in '.css' or '.js' 01208 } 01209 return substr( $subpage, 0, $lastdot ); 01210 } 01211 01217 public function isCssSubpage() { 01218 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01219 && $this->hasContentModel( CONTENT_MODEL_CSS ) ); 01220 } 01221 01227 public function isJsSubpage() { 01228 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01229 && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01230 } 01231 01237 public function isTalkPage() { 01238 return MWNamespace::isTalk( $this->getNamespace() ); 01239 } 01240 01246 public function getTalkPage() { 01247 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); 01248 } 01249 01256 public function getSubjectPage() { 01257 // Is this the same title? 01258 $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); 01259 if ( $this->getNamespace() == $subjectNS ) { 01260 return $this; 01261 } 01262 return Title::makeTitle( $subjectNS, $this->getDBkey() ); 01263 } 01264 01270 public function getDefaultNamespace() { 01271 return $this->mDefaultNamespace; 01272 } 01273 01280 public function getIndexTitle() { 01281 return Title::indexTitle( $this->mNamespace, $this->mTextform ); 01282 } 01283 01291 public function getFragment() { 01292 return $this->mFragment; 01293 } 01294 01301 public function hasFragment() { 01302 return $this->mFragment !== ''; 01303 } 01304 01309 public function getFragmentForURL() { 01310 if ( !$this->hasFragment() ) { 01311 return ''; 01312 } else { 01313 return '#' . Title::escapeFragmentForURL( $this->getFragment() ); 01314 } 01315 } 01316 01327 public function setFragment( $fragment ) { 01328 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); 01329 } 01330 01339 private function prefix( $name ) { 01340 $p = ''; 01341 if ( $this->isExternal() ) { 01342 $p = $this->mInterwiki . ':'; 01343 } 01344 01345 if ( 0 != $this->mNamespace ) { 01346 $p .= $this->getNsText() . ':'; 01347 } 01348 return $p . $name; 01349 } 01350 01357 public function getPrefixedDBkey() { 01358 $s = $this->prefix( $this->mDbkeyform ); 01359 $s = str_replace( ' ', '_', $s ); 01360 return $s; 01361 } 01362 01369 public function getPrefixedText() { 01370 if ( $this->mPrefixedText === null ) { 01371 $s = $this->prefix( $this->mTextform ); 01372 $s = str_replace( '_', ' ', $s ); 01373 $this->mPrefixedText = $s; 01374 } 01375 return $this->mPrefixedText; 01376 } 01377 01383 public function __toString() { 01384 return $this->getPrefixedText(); 01385 } 01386 01393 public function getFullText() { 01394 $text = $this->getPrefixedText(); 01395 if ( $this->hasFragment() ) { 01396 $text .= '#' . $this->getFragment(); 01397 } 01398 return $text; 01399 } 01400 01413 public function getRootText() { 01414 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01415 return $this->getText(); 01416 } 01417 01418 return strtok( $this->getText(), '/' ); 01419 } 01420 01433 public function getRootTitle() { 01434 return Title::makeTitle( $this->getNamespace(), $this->getRootText() ); 01435 } 01436 01448 public function getBaseText() { 01449 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01450 return $this->getText(); 01451 } 01452 01453 $parts = explode( '/', $this->getText() ); 01454 # Don't discard the real title if there's no subpage involved 01455 if ( count( $parts ) > 1 ) { 01456 unset( $parts[count( $parts ) - 1] ); 01457 } 01458 return implode( '/', $parts ); 01459 } 01460 01473 public function getBaseTitle() { 01474 return Title::makeTitle( $this->getNamespace(), $this->getBaseText() ); 01475 } 01476 01488 public function getSubpageText() { 01489 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01490 return $this->mTextform; 01491 } 01492 $parts = explode( '/', $this->mTextform ); 01493 return $parts[count( $parts ) - 1]; 01494 } 01495 01509 public function getSubpage( $text ) { 01510 return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text ); 01511 } 01512 01520 public function getEscapedText() { 01521 wfDeprecated( __METHOD__, '1.19' ); 01522 return htmlspecialchars( $this->getPrefixedText() ); 01523 } 01524 01530 public function getSubpageUrlForm() { 01531 $text = $this->getSubpageText(); 01532 $text = wfUrlencode( str_replace( ' ', '_', $text ) ); 01533 return $text; 01534 } 01535 01541 public function getPrefixedURL() { 01542 $s = $this->prefix( $this->mDbkeyform ); 01543 $s = wfUrlencode( str_replace( ' ', '_', $s ) ); 01544 return $s; 01545 } 01546 01560 private static function fixUrlQueryArgs( $query, $query2 = false ) { 01561 if ( $query2 !== false ) { 01562 wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " . 01563 "method called with a second parameter is deprecated. Add your " . 01564 "parameter to an array passed as the first parameter.", "1.19" ); 01565 } 01566 if ( is_array( $query ) ) { 01567 $query = wfArrayToCgi( $query ); 01568 } 01569 if ( $query2 ) { 01570 if ( is_string( $query2 ) ) { 01571 // $query2 is a string, we will consider this to be 01572 // a deprecated $variant argument and add it to the query 01573 $query2 = wfArrayToCgi( array( 'variant' => $query2 ) ); 01574 } else { 01575 $query2 = wfArrayToCgi( $query2 ); 01576 } 01577 // If we have $query content add a & to it first 01578 if ( $query ) { 01579 $query .= '&'; 01580 } 01581 // Now append the queries together 01582 $query .= $query2; 01583 } 01584 return $query; 01585 } 01586 01598 public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01599 $query = self::fixUrlQueryArgs( $query, $query2 ); 01600 01601 # Hand off all the decisions on urls to getLocalURL 01602 $url = $this->getLocalURL( $query ); 01603 01604 # Expand the url to make it a full url. Note that getLocalURL has the 01605 # potential to output full urls for a variety of reasons, so we use 01606 # wfExpandUrl instead of simply prepending $wgServer 01607 $url = wfExpandUrl( $url, $proto ); 01608 01609 # Finally, add the fragment. 01610 $url .= $this->getFragmentForURL(); 01611 01612 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); 01613 return $url; 01614 } 01615 01637 public function getLocalURL( $query = '', $query2 = false ) { 01638 global $wgArticlePath, $wgScript, $wgServer, $wgRequest; 01639 01640 $query = self::fixUrlQueryArgs( $query, $query2 ); 01641 01642 $interwiki = Interwiki::fetch( $this->mInterwiki ); 01643 if ( $interwiki ) { 01644 $namespace = $this->getNsText(); 01645 if ( $namespace != '' ) { 01646 # Can this actually happen? Interwikis shouldn't be parsed. 01647 # Yes! It can in interwiki transclusion. But... it probably shouldn't. 01648 $namespace .= ':'; 01649 } 01650 $url = $interwiki->getURL( $namespace . $this->getDBkey() ); 01651 $url = wfAppendQuery( $url, $query ); 01652 } else { 01653 $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); 01654 if ( $query == '' ) { 01655 $url = str_replace( '$1', $dbkey, $wgArticlePath ); 01656 wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); 01657 } else { 01658 global $wgVariantArticlePath, $wgActionPaths, $wgContLang; 01659 $url = false; 01660 $matches = array(); 01661 01662 if ( !empty( $wgActionPaths ) 01663 && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) 01664 ) { 01665 $action = urldecode( $matches[2] ); 01666 if ( isset( $wgActionPaths[$action] ) ) { 01667 $query = $matches[1]; 01668 if ( isset( $matches[4] ) ) { 01669 $query .= $matches[4]; 01670 } 01671 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); 01672 if ( $query != '' ) { 01673 $url = wfAppendQuery( $url, $query ); 01674 } 01675 } 01676 } 01677 01678 if ( $url === false 01679 && $wgVariantArticlePath 01680 && $wgContLang->getCode() === $this->getPageLanguage()->getCode() 01681 && $this->getPageLanguage()->hasVariants() 01682 && preg_match( '/^variant=([^&]*)$/', $query, $matches ) 01683 ) { 01684 $variant = urldecode( $matches[1] ); 01685 if ( $this->getPageLanguage()->hasVariant( $variant ) ) { 01686 // Only do the variant replacement if the given variant is a valid 01687 // variant for the page's language. 01688 $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); 01689 $url = str_replace( '$1', $dbkey, $url ); 01690 } 01691 } 01692 01693 if ( $url === false ) { 01694 if ( $query == '-' ) { 01695 $query = ''; 01696 } 01697 $url = "{$wgScript}?title={$dbkey}&{$query}"; 01698 } 01699 } 01700 01701 wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) ); 01702 01703 // @todo FIXME: This causes breakage in various places when we 01704 // actually expected a local URL and end up with dupe prefixes. 01705 if ( $wgRequest->getVal( 'action' ) == 'render' ) { 01706 $url = $wgServer . $url; 01707 } 01708 } 01709 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) ); 01710 return $url; 01711 } 01712 01729 public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01730 wfProfileIn( __METHOD__ ); 01731 if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) { 01732 $ret = $this->getFullURL( $query, $query2, $proto ); 01733 } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) { 01734 $ret = $this->getFragmentForURL(); 01735 } else { 01736 $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL(); 01737 } 01738 wfProfileOut( __METHOD__ ); 01739 return $ret; 01740 } 01741 01752 public function escapeLocalURL( $query = '', $query2 = false ) { 01753 wfDeprecated( __METHOD__, '1.19' ); 01754 return htmlspecialchars( $this->getLocalURL( $query, $query2 ) ); 01755 } 01756 01765 public function escapeFullURL( $query = '', $query2 = false ) { 01766 wfDeprecated( __METHOD__, '1.19' ); 01767 return htmlspecialchars( $this->getFullURL( $query, $query2 ) ); 01768 } 01769 01782 public function getInternalURL( $query = '', $query2 = false ) { 01783 global $wgInternalServer, $wgServer; 01784 $query = self::fixUrlQueryArgs( $query, $query2 ); 01785 $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; 01786 $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP ); 01787 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); 01788 return $url; 01789 } 01790 01802 public function getCanonicalURL( $query = '', $query2 = false ) { 01803 $query = self::fixUrlQueryArgs( $query, $query2 ); 01804 $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL ); 01805 wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) ); 01806 return $url; 01807 } 01808 01817 public function escapeCanonicalURL( $query = '', $query2 = false ) { 01818 wfDeprecated( __METHOD__, '1.19' ); 01819 return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) ); 01820 } 01821 01828 public function getEditURL() { 01829 if ( $this->isExternal() ) { 01830 return ''; 01831 } 01832 $s = $this->getLocalURL( 'action=edit' ); 01833 01834 return $s; 01835 } 01836 01843 public function userIsWatching() { 01844 global $wgUser; 01845 01846 if ( is_null( $this->mWatched ) ) { 01847 if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) { 01848 $this->mWatched = false; 01849 } else { 01850 $this->mWatched = $wgUser->isWatched( $this ); 01851 } 01852 } 01853 return $this->mWatched; 01854 } 01855 01862 public function userCanRead() { 01863 wfDeprecated( __METHOD__, '1.19' ); 01864 return $this->userCan( 'read' ); 01865 } 01866 01882 public function quickUserCan( $action, $user = null ) { 01883 return $this->userCan( $action, $user, false ); 01884 } 01885 01896 public function userCan( $action, $user = null, $doExpensiveQueries = true ) { 01897 if ( !$user instanceof User ) { 01898 global $wgUser; 01899 $user = $wgUser; 01900 } 01901 return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) ); 01902 } 01903 01917 public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { 01918 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); 01919 01920 // Remove the errors being ignored. 01921 foreach ( $errors as $index => $error ) { 01922 $error_key = is_array( $error ) ? $error[0] : $error; 01923 01924 if ( in_array( $error_key, $ignoreErrors ) ) { 01925 unset( $errors[$index] ); 01926 } 01927 } 01928 01929 return $errors; 01930 } 01931 01943 private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01944 if ( !wfRunHooks( 'TitleQuickPermissions', array( $this, $user, $action, &$errors, $doExpensiveQueries, $short ) ) ) { 01945 return $errors; 01946 } 01947 01948 if ( $action == 'create' ) { 01949 if ( 01950 ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || 01951 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) 01952 ) { 01953 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); 01954 } 01955 } elseif ( $action == 'move' ) { 01956 if ( !$user->isAllowed( 'move-rootuserpages' ) 01957 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01958 // Show user page-specific message only if the user can move other pages 01959 $errors[] = array( 'cant-move-user-page' ); 01960 } 01961 01962 // Check if user is allowed to move files if it's a file 01963 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { 01964 $errors[] = array( 'movenotallowedfile' ); 01965 } 01966 01967 if ( !$user->isAllowed( 'move' ) ) { 01968 // User can't move anything 01969 $userCanMove = User::groupHasPermission( 'user', 'move' ); 01970 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); 01971 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { 01972 // custom message if logged-in users without any special rights can move 01973 $errors[] = array( 'movenologintext' ); 01974 } else { 01975 $errors[] = array( 'movenotallowed' ); 01976 } 01977 } 01978 } elseif ( $action == 'move-target' ) { 01979 if ( !$user->isAllowed( 'move' ) ) { 01980 // User can't move anything 01981 $errors[] = array( 'movenotallowed' ); 01982 } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 01983 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01984 // Show user page-specific message only if the user can move other pages 01985 $errors[] = array( 'cant-move-to-user-page' ); 01986 } 01987 } elseif ( !$user->isAllowed( $action ) ) { 01988 $errors[] = $this->missingPermissionError( $action, $short ); 01989 } 01990 01991 return $errors; 01992 } 01993 02002 private function resultToError( $errors, $result ) { 02003 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { 02004 // A single array representing an error 02005 $errors[] = $result; 02006 } elseif ( is_array( $result ) && is_array( $result[0] ) ) { 02007 // A nested array representing multiple errors 02008 $errors = array_merge( $errors, $result ); 02009 } elseif ( $result !== '' && is_string( $result ) ) { 02010 // A string representing a message-id 02011 $errors[] = array( $result ); 02012 } elseif ( $result === false ) { 02013 // a generic "We don't want them to do that" 02014 $errors[] = array( 'badaccess-group0' ); 02015 } 02016 return $errors; 02017 } 02018 02030 private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { 02031 // Use getUserPermissionsErrors instead 02032 $result = ''; 02033 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { 02034 return $result ? array() : array( array( 'badaccess-group0' ) ); 02035 } 02036 // Check getUserPermissionsErrors hook 02037 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { 02038 $errors = $this->resultToError( $errors, $result ); 02039 } 02040 // Check getUserPermissionsErrorsExpensive hook 02041 if ( 02042 $doExpensiveQueries 02043 && !( $short && count( $errors ) > 0 ) 02044 && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) 02045 ) { 02046 $errors = $this->resultToError( $errors, $result ); 02047 } 02048 02049 return $errors; 02050 } 02051 02063 private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02064 # Only 'createaccount' can be performed on special pages, 02065 # which don't actually exist in the DB. 02066 if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) { 02067 $errors[] = array( 'ns-specialprotected' ); 02068 } 02069 02070 # Check $wgNamespaceProtection for restricted namespaces 02071 if ( $this->isNamespaceProtected( $user ) ) { 02072 $ns = $this->mNamespace == NS_MAIN ? 02073 wfMessage( 'nstab-main' )->text() : $this->getNsText(); 02074 $errors[] = $this->mNamespace == NS_MEDIAWIKI ? 02075 array( 'protectedinterface' ) : array( 'namespaceprotected', $ns ); 02076 } 02077 02078 return $errors; 02079 } 02080 02092 private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02093 # Protect css/js subpages of user pages 02094 # XXX: this might be better using restrictions 02095 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only 02096 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) { 02097 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { 02098 if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { 02099 $errors[] = array( 'mycustomcssprotected' ); 02100 } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) { 02101 $errors[] = array( 'mycustomjsprotected' ); 02102 } 02103 } else { 02104 if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { 02105 $errors[] = array( 'customcssprotected' ); 02106 } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { 02107 $errors[] = array( 'customjsprotected' ); 02108 } 02109 } 02110 } 02111 02112 return $errors; 02113 } 02114 02128 private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02129 foreach ( $this->getRestrictions( $action ) as $right ) { 02130 // Backwards compatibility, rewrite sysop -> editprotected 02131 if ( $right == 'sysop' ) { 02132 $right = 'editprotected'; 02133 } 02134 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected 02135 if ( $right == 'autoconfirmed' ) { 02136 $right = 'editsemiprotected'; 02137 } 02138 if ( $right == '' ) { 02139 continue; 02140 } 02141 if ( !$user->isAllowed( $right ) ) { 02142 $errors[] = array( 'protectedpagetext', $right ); 02143 } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) { 02144 $errors[] = array( 'protectedpagetext', 'protect' ); 02145 } 02146 } 02147 02148 return $errors; 02149 } 02150 02162 private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02163 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { 02164 # We /could/ use the protection level on the source page, but it's 02165 # fairly ugly as we have to establish a precedence hierarchy for pages 02166 # included by multiple cascade-protected pages. So just restrict 02167 # it to people with 'protect' permission, as they could remove the 02168 # protection anyway. 02169 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); 02170 # Cascading protection depends on more than this page... 02171 # Several cascading protected pages may include this page... 02172 # Check each cascading level 02173 # This is only for protection restrictions, not for all actions 02174 if ( isset( $restrictions[$action] ) ) { 02175 foreach ( $restrictions[$action] as $right ) { 02176 // Backwards compatibility, rewrite sysop -> editprotected 02177 if ( $right == 'sysop' ) { 02178 $right = 'editprotected'; 02179 } 02180 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected 02181 if ( $right == 'autoconfirmed' ) { 02182 $right = 'editsemiprotected'; 02183 } 02184 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { 02185 $pages = ''; 02186 foreach ( $cascadingSources as $page ) { 02187 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; 02188 } 02189 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); 02190 } 02191 } 02192 } 02193 } 02194 02195 return $errors; 02196 } 02197 02209 private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02210 global $wgDeleteRevisionsLimit, $wgLang; 02211 02212 if ( $action == 'protect' ) { 02213 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) { 02214 // If they can't edit, they shouldn't protect. 02215 $errors[] = array( 'protect-cantedit' ); 02216 } 02217 } elseif ( $action == 'create' ) { 02218 $title_protection = $this->getTitleProtection(); 02219 if ( $title_protection ) { 02220 if ( $title_protection['pt_create_perm'] == 'sysop' ) { 02221 $title_protection['pt_create_perm'] = 'editprotected'; // B/C 02222 } 02223 if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) { 02224 $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C 02225 } 02226 if ( $title_protection['pt_create_perm'] == '' 02227 || !$user->isAllowed( $title_protection['pt_create_perm'] ) 02228 ) { 02229 $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] ); 02230 } 02231 } 02232 } elseif ( $action == 'move' ) { 02233 // Check for immobile pages 02234 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02235 // Specific message for this case 02236 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 02237 } elseif ( !$this->isMovable() ) { 02238 // Less specific message for rarer cases 02239 $errors[] = array( 'immobile-source-page' ); 02240 } 02241 } elseif ( $action == 'move-target' ) { 02242 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02243 $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); 02244 } elseif ( !$this->isMovable() ) { 02245 $errors[] = array( 'immobile-target-page' ); 02246 } 02247 } elseif ( $action == 'delete' ) { 02248 if ( $doExpensiveQueries && $wgDeleteRevisionsLimit 02249 && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() 02250 ) { 02251 $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); 02252 } 02253 } 02254 return $errors; 02255 } 02256 02268 private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { 02269 // Account creation blocks handled at userlogin. 02270 // Unblocking handled in SpecialUnblock 02271 if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { 02272 return $errors; 02273 } 02274 02275 global $wgEmailConfirmToEdit; 02276 02277 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { 02278 $errors[] = array( 'confirmedittext' ); 02279 } 02280 02281 if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) { 02282 // Don't block the user from editing their own talk page unless they've been 02283 // explicitly blocked from that too. 02284 } elseif ( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { 02285 // @todo FIXME: Pass the relevant context into this function. 02286 $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() ); 02287 } 02288 02289 return $errors; 02290 } 02291 02303 private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02304 global $wgWhitelistRead, $wgWhitelistReadRegexp; 02305 02306 $whitelisted = false; 02307 if ( User::isEveryoneAllowed( 'read' ) ) { 02308 # Shortcut for public wikis, allows skipping quite a bit of code 02309 $whitelisted = true; 02310 } elseif ( $user->isAllowed( 'read' ) ) { 02311 # If the user is allowed to read pages, he is allowed to read all pages 02312 $whitelisted = true; 02313 } elseif ( $this->isSpecial( 'Userlogin' ) 02314 || $this->isSpecial( 'ChangePassword' ) 02315 || $this->isSpecial( 'PasswordReset' ) 02316 ) { 02317 # Always grant access to the login page. 02318 # Even anons need to be able to log in. 02319 $whitelisted = true; 02320 } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { 02321 # Time to check the whitelist 02322 # Only do these checks is there's something to check against 02323 $name = $this->getPrefixedText(); 02324 $dbName = $this->getPrefixedDBkey(); 02325 02326 // Check for explicit whitelisting with and without underscores 02327 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { 02328 $whitelisted = true; 02329 } elseif ( $this->getNamespace() == NS_MAIN ) { 02330 # Old settings might have the title prefixed with 02331 # a colon for main-namespace pages 02332 if ( in_array( ':' . $name, $wgWhitelistRead ) ) { 02333 $whitelisted = true; 02334 } 02335 } elseif ( $this->isSpecialPage() ) { 02336 # If it's a special page, ditch the subpage bit and check again 02337 $name = $this->getDBkey(); 02338 list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); 02339 if ( $name ) { 02340 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); 02341 if ( in_array( $pure, $wgWhitelistRead, true ) ) { 02342 $whitelisted = true; 02343 } 02344 } 02345 } 02346 } 02347 02348 if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { 02349 $name = $this->getPrefixedText(); 02350 // Check for regex whitelisting 02351 foreach ( $wgWhitelistReadRegexp as $listItem ) { 02352 if ( preg_match( $listItem, $name ) ) { 02353 $whitelisted = true; 02354 break; 02355 } 02356 } 02357 } 02358 02359 if ( !$whitelisted ) { 02360 # If the title is not whitelisted, give extensions a chance to do so... 02361 wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); 02362 if ( !$whitelisted ) { 02363 $errors[] = $this->missingPermissionError( $action, $short ); 02364 } 02365 } 02366 02367 return $errors; 02368 } 02369 02378 private function missingPermissionError( $action, $short ) { 02379 // We avoid expensive display logic for quickUserCan's and such 02380 if ( $short ) { 02381 return array( 'badaccess-group0' ); 02382 } 02383 02384 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), 02385 User::getGroupsWithPermission( $action ) ); 02386 02387 if ( count( $groups ) ) { 02388 global $wgLang; 02389 return array( 02390 'badaccess-groups', 02391 $wgLang->commaList( $groups ), 02392 count( $groups ) 02393 ); 02394 } else { 02395 return array( 'badaccess-group0' ); 02396 } 02397 } 02398 02410 protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { 02411 wfProfileIn( __METHOD__ ); 02412 02413 # Read has special handling 02414 if ( $action == 'read' ) { 02415 $checks = array( 02416 'checkPermissionHooks', 02417 'checkReadPermissions', 02418 ); 02419 } else { 02420 $checks = array( 02421 'checkQuickPermissions', 02422 'checkPermissionHooks', 02423 'checkSpecialsAndNSPermissions', 02424 'checkCSSandJSPermissions', 02425 'checkPageRestrictions', 02426 'checkCascadingSourcesRestrictions', 02427 'checkActionPermissions', 02428 'checkUserBlock' 02429 ); 02430 } 02431 02432 $errors = array(); 02433 while ( count( $checks ) > 0 && 02434 !( $short && count( $errors ) > 0 ) ) { 02435 $method = array_shift( $checks ); 02436 $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); 02437 } 02438 02439 wfProfileOut( __METHOD__ ); 02440 return $errors; 02441 } 02442 02450 public static function getFilteredRestrictionTypes( $exists = true ) { 02451 global $wgRestrictionTypes; 02452 $types = $wgRestrictionTypes; 02453 if ( $exists ) { 02454 # Remove the create restriction for existing titles 02455 $types = array_diff( $types, array( 'create' ) ); 02456 } else { 02457 # Only the create and upload restrictions apply to non-existing titles 02458 $types = array_intersect( $types, array( 'create', 'upload' ) ); 02459 } 02460 return $types; 02461 } 02462 02468 public function getRestrictionTypes() { 02469 if ( $this->isSpecialPage() ) { 02470 return array(); 02471 } 02472 02473 $types = self::getFilteredRestrictionTypes( $this->exists() ); 02474 02475 if ( $this->getNamespace() != NS_FILE ) { 02476 # Remove the upload restriction for non-file titles 02477 $types = array_diff( $types, array( 'upload' ) ); 02478 } 02479 02480 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); 02481 02482 wfDebug( __METHOD__ . ': applicable restrictions to [[' . 02483 $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); 02484 02485 return $types; 02486 } 02487 02495 private function getTitleProtection() { 02496 // Can't protect pages in special namespaces 02497 if ( $this->getNamespace() < 0 ) { 02498 return false; 02499 } 02500 02501 // Can't protect pages that exist. 02502 if ( $this->exists() ) { 02503 return false; 02504 } 02505 02506 if ( !isset( $this->mTitleProtection ) ) { 02507 $dbr = wfGetDB( DB_SLAVE ); 02508 $res = $dbr->select( 02509 'protected_titles', 02510 array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ), 02511 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02512 __METHOD__ 02513 ); 02514 02515 // fetchRow returns false if there are no rows. 02516 $this->mTitleProtection = $dbr->fetchRow( $res ); 02517 } 02518 return $this->mTitleProtection; 02519 } 02520 02530 public function updateTitleProtection( $create_perm, $reason, $expiry ) { 02531 wfDeprecated( __METHOD__, '1.19' ); 02532 02533 global $wgUser; 02534 02535 $limit = array( 'create' => $create_perm ); 02536 $expiry = array( 'create' => $expiry ); 02537 02538 $page = WikiPage::factory( $this ); 02539 $cascade = false; 02540 $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $wgUser ); 02541 02542 return $status->isOK(); 02543 } 02544 02548 public function deleteTitleProtection() { 02549 $dbw = wfGetDB( DB_MASTER ); 02550 02551 $dbw->delete( 02552 'protected_titles', 02553 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02554 __METHOD__ 02555 ); 02556 $this->mTitleProtection = false; 02557 } 02558 02566 public function isSemiProtected( $action = 'edit' ) { 02567 global $wgSemiprotectedRestrictionLevels; 02568 02569 $restrictions = $this->getRestrictions( $action ); 02570 $semi = $wgSemiprotectedRestrictionLevels; 02571 if ( !$restrictions || !$semi ) { 02572 // Not protected, or all protection is full protection 02573 return false; 02574 } 02575 02576 // Remap autoconfirmed to editsemiprotected for BC 02577 foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) { 02578 $semi[$key] = 'editsemiprotected'; 02579 } 02580 foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) { 02581 $restrictions[$key] = 'editsemiprotected'; 02582 } 02583 02584 return !array_diff( $restrictions, $semi ); 02585 } 02586 02594 public function isProtected( $action = '' ) { 02595 global $wgRestrictionLevels; 02596 02597 $restrictionTypes = $this->getRestrictionTypes(); 02598 02599 # Special pages have inherent protection 02600 if ( $this->isSpecialPage() ) { 02601 return true; 02602 } 02603 02604 # Check regular protection levels 02605 foreach ( $restrictionTypes as $type ) { 02606 if ( $action == $type || $action == '' ) { 02607 $r = $this->getRestrictions( $type ); 02608 foreach ( $wgRestrictionLevels as $level ) { 02609 if ( in_array( $level, $r ) && $level != '' ) { 02610 return true; 02611 } 02612 } 02613 } 02614 } 02615 02616 return false; 02617 } 02618 02626 public function isNamespaceProtected( User $user ) { 02627 global $wgNamespaceProtection; 02628 02629 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { 02630 foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { 02631 if ( $right != '' && !$user->isAllowed( $right ) ) { 02632 return true; 02633 } 02634 } 02635 } 02636 return false; 02637 } 02638 02644 public function isCascadeProtected() { 02645 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); 02646 return ( $sources > 0 ); 02647 } 02648 02658 public function areCascadeProtectionSourcesLoaded( $getPages = true ) { 02659 return $getPages ? isset( $this->mCascadeSources ) : isset( $this->mHasCascadingRestrictions ); 02660 } 02661 02672 public function getCascadeProtectionSources( $getPages = true ) { 02673 global $wgContLang; 02674 $pagerestrictions = array(); 02675 02676 if ( isset( $this->mCascadeSources ) && $getPages ) { 02677 return array( $this->mCascadeSources, $this->mCascadingRestrictions ); 02678 } elseif ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) { 02679 return array( $this->mHasCascadingRestrictions, $pagerestrictions ); 02680 } 02681 02682 wfProfileIn( __METHOD__ ); 02683 02684 $dbr = wfGetDB( DB_SLAVE ); 02685 02686 if ( $this->getNamespace() == NS_FILE ) { 02687 $tables = array( 'imagelinks', 'page_restrictions' ); 02688 $where_clauses = array( 02689 'il_to' => $this->getDBkey(), 02690 'il_from=pr_page', 02691 'pr_cascade' => 1 02692 ); 02693 } else { 02694 $tables = array( 'templatelinks', 'page_restrictions' ); 02695 $where_clauses = array( 02696 'tl_namespace' => $this->getNamespace(), 02697 'tl_title' => $this->getDBkey(), 02698 'tl_from=pr_page', 02699 'pr_cascade' => 1 02700 ); 02701 } 02702 02703 if ( $getPages ) { 02704 $cols = array( 'pr_page', 'page_namespace', 'page_title', 02705 'pr_expiry', 'pr_type', 'pr_level' ); 02706 $where_clauses[] = 'page_id=pr_page'; 02707 $tables[] = 'page'; 02708 } else { 02709 $cols = array( 'pr_expiry' ); 02710 } 02711 02712 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); 02713 02714 $sources = $getPages ? array() : false; 02715 $now = wfTimestampNow(); 02716 $purgeExpired = false; 02717 02718 foreach ( $res as $row ) { 02719 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02720 if ( $expiry > $now ) { 02721 if ( $getPages ) { 02722 $page_id = $row->pr_page; 02723 $page_ns = $row->page_namespace; 02724 $page_title = $row->page_title; 02725 $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); 02726 # Add groups needed for each restriction type if its not already there 02727 # Make sure this restriction type still exists 02728 02729 if ( !isset( $pagerestrictions[$row->pr_type] ) ) { 02730 $pagerestrictions[$row->pr_type] = array(); 02731 } 02732 02733 if ( 02734 isset( $pagerestrictions[$row->pr_type] ) 02735 && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) 02736 ) { 02737 $pagerestrictions[$row->pr_type][] = $row->pr_level; 02738 } 02739 } else { 02740 $sources = true; 02741 } 02742 } else { 02743 // Trigger lazy purge of expired restrictions from the db 02744 $purgeExpired = true; 02745 } 02746 } 02747 if ( $purgeExpired ) { 02748 Title::purgeExpiredRestrictions(); 02749 } 02750 02751 if ( $getPages ) { 02752 $this->mCascadeSources = $sources; 02753 $this->mCascadingRestrictions = $pagerestrictions; 02754 } else { 02755 $this->mHasCascadingRestrictions = $sources; 02756 } 02757 02758 wfProfileOut( __METHOD__ ); 02759 return array( $sources, $pagerestrictions ); 02760 } 02761 02769 public function areRestrictionsLoaded() { 02770 return $this->mRestrictionsLoaded; 02771 } 02772 02779 public function getRestrictions( $action ) { 02780 if ( !$this->mRestrictionsLoaded ) { 02781 $this->loadRestrictions(); 02782 } 02783 return isset( $this->mRestrictions[$action] ) 02784 ? $this->mRestrictions[$action] 02785 : array(); 02786 } 02787 02796 public function getAllRestrictions() { 02797 if ( !$this->mRestrictionsLoaded ) { 02798 $this->loadRestrictions(); 02799 } 02800 return $this->mRestrictions; 02801 } 02802 02810 public function getRestrictionExpiry( $action ) { 02811 if ( !$this->mRestrictionsLoaded ) { 02812 $this->loadRestrictions(); 02813 } 02814 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; 02815 } 02816 02822 function areRestrictionsCascading() { 02823 if ( !$this->mRestrictionsLoaded ) { 02824 $this->loadRestrictions(); 02825 } 02826 02827 return $this->mCascadeRestriction; 02828 } 02829 02837 private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { 02838 $rows = array(); 02839 02840 foreach ( $res as $row ) { 02841 $rows[] = $row; 02842 } 02843 02844 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); 02845 } 02846 02856 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { 02857 global $wgContLang; 02858 $dbr = wfGetDB( DB_SLAVE ); 02859 02860 $restrictionTypes = $this->getRestrictionTypes(); 02861 02862 foreach ( $restrictionTypes as $type ) { 02863 $this->mRestrictions[$type] = array(); 02864 $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); 02865 } 02866 02867 $this->mCascadeRestriction = false; 02868 02869 # Backwards-compatibility: also load the restrictions from the page record (old format). 02870 02871 if ( $oldFashionedRestrictions === null ) { 02872 $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', 02873 array( 'page_id' => $this->getArticleID() ), __METHOD__ ); 02874 } 02875 02876 if ( $oldFashionedRestrictions != '' ) { 02877 02878 foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { 02879 $temp = explode( '=', trim( $restrict ) ); 02880 if ( count( $temp ) == 1 ) { 02881 // old old format should be treated as edit/move restriction 02882 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); 02883 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); 02884 } else { 02885 $restriction = trim( $temp[1] ); 02886 if ( $restriction != '' ) { //some old entries are empty 02887 $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); 02888 } 02889 } 02890 } 02891 02892 $this->mOldRestrictions = true; 02893 02894 } 02895 02896 if ( count( $rows ) ) { 02897 # Current system - load second to make them override. 02898 $now = wfTimestampNow(); 02899 $purgeExpired = false; 02900 02901 # Cycle through all the restrictions. 02902 foreach ( $rows as $row ) { 02903 02904 // Don't take care of restrictions types that aren't allowed 02905 if ( !in_array( $row->pr_type, $restrictionTypes ) ) { 02906 continue; 02907 } 02908 02909 // This code should be refactored, now that it's being used more generally, 02910 // But I don't really see any harm in leaving it in Block for now -werdna 02911 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02912 02913 // Only apply the restrictions if they haven't expired! 02914 if ( !$expiry || $expiry > $now ) { 02915 $this->mRestrictionsExpiry[$row->pr_type] = $expiry; 02916 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); 02917 02918 $this->mCascadeRestriction |= $row->pr_cascade; 02919 } else { 02920 // Trigger a lazy purge of expired restrictions 02921 $purgeExpired = true; 02922 } 02923 } 02924 02925 if ( $purgeExpired ) { 02926 Title::purgeExpiredRestrictions(); 02927 } 02928 } 02929 02930 $this->mRestrictionsLoaded = true; 02931 } 02932 02939 public function loadRestrictions( $oldFashionedRestrictions = null ) { 02940 global $wgContLang; 02941 if ( !$this->mRestrictionsLoaded ) { 02942 if ( $this->exists() ) { 02943 $dbr = wfGetDB( DB_SLAVE ); 02944 02945 $res = $dbr->select( 02946 'page_restrictions', 02947 array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ), 02948 array( 'pr_page' => $this->getArticleID() ), 02949 __METHOD__ 02950 ); 02951 02952 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); 02953 } else { 02954 $title_protection = $this->getTitleProtection(); 02955 02956 if ( $title_protection ) { 02957 $now = wfTimestampNow(); 02958 $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); 02959 02960 if ( !$expiry || $expiry > $now ) { 02961 // Apply the restrictions 02962 $this->mRestrictionsExpiry['create'] = $expiry; 02963 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); 02964 } else { // Get rid of the old restrictions 02965 Title::purgeExpiredRestrictions(); 02966 $this->mTitleProtection = false; 02967 } 02968 } else { 02969 $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); 02970 } 02971 $this->mRestrictionsLoaded = true; 02972 } 02973 } 02974 } 02975 02980 public function flushRestrictions() { 02981 $this->mRestrictionsLoaded = false; 02982 $this->mTitleProtection = null; 02983 } 02984 02988 static function purgeExpiredRestrictions() { 02989 if ( wfReadOnly() ) { 02990 return; 02991 } 02992 02993 $method = __METHOD__; 02994 $dbw = wfGetDB( DB_MASTER ); 02995 $dbw->onTransactionIdle( function() use ( $dbw, $method ) { 02996 $dbw->delete( 02997 'page_restrictions', 02998 array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02999 $method 03000 ); 03001 $dbw->delete( 03002 'protected_titles', 03003 array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 03004 $method 03005 ); 03006 } ); 03007 } 03008 03014 public function hasSubpages() { 03015 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 03016 # Duh 03017 return false; 03018 } 03019 03020 # We dynamically add a member variable for the purpose of this method 03021 # alone to cache the result. There's no point in having it hanging 03022 # around uninitialized in every Title object; therefore we only add it 03023 # if needed and don't declare it statically. 03024 if ( !isset( $this->mHasSubpages ) ) { 03025 $this->mHasSubpages = false; 03026 $subpages = $this->getSubpages( 1 ); 03027 if ( $subpages instanceof TitleArray ) { 03028 $this->mHasSubpages = (bool)$subpages->count(); 03029 } 03030 } 03031 03032 return $this->mHasSubpages; 03033 } 03034 03042 public function getSubpages( $limit = -1 ) { 03043 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03044 return array(); 03045 } 03046 03047 $dbr = wfGetDB( DB_SLAVE ); 03048 $conds['page_namespace'] = $this->getNamespace(); 03049 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); 03050 $options = array(); 03051 if ( $limit > -1 ) { 03052 $options['LIMIT'] = $limit; 03053 } 03054 $this->mSubpages = TitleArray::newFromResult( 03055 $dbr->select( 'page', 03056 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), 03057 $conds, 03058 __METHOD__, 03059 $options 03060 ) 03061 ); 03062 return $this->mSubpages; 03063 } 03064 03070 public function isDeleted() { 03071 if ( $this->getNamespace() < 0 ) { 03072 $n = 0; 03073 } else { 03074 $dbr = wfGetDB( DB_SLAVE ); 03075 03076 $n = $dbr->selectField( 'archive', 'COUNT(*)', 03077 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 03078 __METHOD__ 03079 ); 03080 if ( $this->getNamespace() == NS_FILE ) { 03081 $n += $dbr->selectField( 'filearchive', 'COUNT(*)', 03082 array( 'fa_name' => $this->getDBkey() ), 03083 __METHOD__ 03084 ); 03085 } 03086 } 03087 return (int)$n; 03088 } 03089 03095 public function isDeletedQuick() { 03096 if ( $this->getNamespace() < 0 ) { 03097 return false; 03098 } 03099 $dbr = wfGetDB( DB_SLAVE ); 03100 $deleted = (bool)$dbr->selectField( 'archive', '1', 03101 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 03102 __METHOD__ 03103 ); 03104 if ( !$deleted && $this->getNamespace() == NS_FILE ) { 03105 $deleted = (bool)$dbr->selectField( 'filearchive', '1', 03106 array( 'fa_name' => $this->getDBkey() ), 03107 __METHOD__ 03108 ); 03109 } 03110 return $deleted; 03111 } 03112 03121 public function getArticleID( $flags = 0 ) { 03122 if ( $this->getNamespace() < 0 ) { 03123 $this->mArticleID = 0; 03124 return $this->mArticleID; 03125 } 03126 $linkCache = LinkCache::singleton(); 03127 if ( $flags & self::GAID_FOR_UPDATE ) { 03128 $oldUpdate = $linkCache->forUpdate( true ); 03129 $linkCache->clearLink( $this ); 03130 $this->mArticleID = $linkCache->addLinkObj( $this ); 03131 $linkCache->forUpdate( $oldUpdate ); 03132 } else { 03133 if ( -1 == $this->mArticleID ) { 03134 $this->mArticleID = $linkCache->addLinkObj( $this ); 03135 } 03136 } 03137 return $this->mArticleID; 03138 } 03139 03147 public function isRedirect( $flags = 0 ) { 03148 if ( !is_null( $this->mRedirect ) ) { 03149 return $this->mRedirect; 03150 } 03151 # Calling getArticleID() loads the field from cache as needed 03152 if ( !$this->getArticleID( $flags ) ) { 03153 $this->mRedirect = false; 03154 return $this->mRedirect; 03155 } 03156 03157 $linkCache = LinkCache::singleton(); 03158 $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' ); 03159 if ( $cached === null ) { 03160 # Trust LinkCache's state over our own 03161 # LinkCache is telling us that the page doesn't exist, despite there being cached 03162 # data relating to an existing page in $this->mArticleID. Updaters should clear 03163 # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is 03164 # set, then LinkCache will definitely be up to date here, since getArticleID() forces 03165 # LinkCache to refresh its data from the master. 03166 $this->mRedirect = false; 03167 return $this->mRedirect; 03168 } 03169 03170 $this->mRedirect = (bool)$cached; 03171 03172 return $this->mRedirect; 03173 } 03174 03182 public function getLength( $flags = 0 ) { 03183 if ( $this->mLength != -1 ) { 03184 return $this->mLength; 03185 } 03186 # Calling getArticleID() loads the field from cache as needed 03187 if ( !$this->getArticleID( $flags ) ) { 03188 $this->mLength = 0; 03189 return $this->mLength; 03190 } 03191 $linkCache = LinkCache::singleton(); 03192 $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' ); 03193 if ( $cached === null ) { 03194 # Trust LinkCache's state over our own, as for isRedirect() 03195 $this->mLength = 0; 03196 return $this->mLength; 03197 } 03198 03199 $this->mLength = intval( $cached ); 03200 03201 return $this->mLength; 03202 } 03203 03210 public function getLatestRevID( $flags = 0 ) { 03211 if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) { 03212 return intval( $this->mLatestID ); 03213 } 03214 # Calling getArticleID() loads the field from cache as needed 03215 if ( !$this->getArticleID( $flags ) ) { 03216 $this->mLatestID = 0; 03217 return $this->mLatestID; 03218 } 03219 $linkCache = LinkCache::singleton(); 03220 $linkCache->addLinkObj( $this ); 03221 $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' ); 03222 if ( $cached === null ) { 03223 # Trust LinkCache's state over our own, as for isRedirect() 03224 $this->mLatestID = 0; 03225 return $this->mLatestID; 03226 } 03227 03228 $this->mLatestID = intval( $cached ); 03229 03230 return $this->mLatestID; 03231 } 03232 03243 public function resetArticleID( $newid ) { 03244 $linkCache = LinkCache::singleton(); 03245 $linkCache->clearLink( $this ); 03246 03247 if ( $newid === false ) { 03248 $this->mArticleID = -1; 03249 } else { 03250 $this->mArticleID = intval( $newid ); 03251 } 03252 $this->mRestrictionsLoaded = false; 03253 $this->mRestrictions = array(); 03254 $this->mRedirect = null; 03255 $this->mLength = -1; 03256 $this->mLatestID = false; 03257 $this->mContentModel = false; 03258 $this->mEstimateRevisions = null; 03259 $this->mPageLanguage = false; 03260 } 03261 03269 public static function capitalize( $text, $ns = NS_MAIN ) { 03270 global $wgContLang; 03271 03272 if ( MWNamespace::isCapitalized( $ns ) ) { 03273 return $wgContLang->ucfirst( $text ); 03274 } else { 03275 return $text; 03276 } 03277 } 03278 03290 private function secureAndSplit() { 03291 # Initialisation 03292 $this->mInterwiki = ''; 03293 $this->mFragment = ''; 03294 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN 03295 03296 $dbkey = $this->mDbkeyform; 03297 03298 try { 03299 // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share 03300 // the parsing code with Title, while avoiding massive refactoring. 03301 // @todo: get rid of secureAndSplit, refactor parsing code. 03302 $parser = $this->getTitleParser(); 03303 $parts = $parser->splitTitleString( $dbkey, $this->getDefaultNamespace() ); 03304 } catch ( MalformedTitleException $ex ) { 03305 return false; 03306 } 03307 03308 # Fill fields 03309 $this->setFragment( '#' . $parts['fragment'] ); 03310 $this->mInterwiki = $parts['interwiki']; 03311 $this->mNamespace = $parts['namespace']; 03312 $this->mUserCaseDBKey = $parts['user_case_dbkey']; 03313 03314 $this->mDbkeyform = $parts['dbkey']; 03315 $this->mUrlform = wfUrlencode( $this->mDbkeyform ); 03316 $this->mTextform = str_replace( '_', ' ', $this->mDbkeyform ); 03317 03318 # We already know that some pages won't be in the database! 03319 if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) { 03320 $this->mArticleID = 0; 03321 } 03322 03323 return true; 03324 } 03325 03338 public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03339 if ( count( $options ) > 0 ) { 03340 $db = wfGetDB( DB_MASTER ); 03341 } else { 03342 $db = wfGetDB( DB_SLAVE ); 03343 } 03344 03345 $res = $db->select( 03346 array( 'page', $table ), 03347 self::getSelectFields(), 03348 array( 03349 "{$prefix}_from=page_id", 03350 "{$prefix}_namespace" => $this->getNamespace(), 03351 "{$prefix}_title" => $this->getDBkey() ), 03352 __METHOD__, 03353 $options 03354 ); 03355 03356 $retVal = array(); 03357 if ( $res->numRows() ) { 03358 $linkCache = LinkCache::singleton(); 03359 foreach ( $res as $row ) { 03360 $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ); 03361 if ( $titleObj ) { 03362 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03363 $retVal[] = $titleObj; 03364 } 03365 } 03366 } 03367 return $retVal; 03368 } 03369 03380 public function getTemplateLinksTo( $options = array() ) { 03381 return $this->getLinksTo( $options, 'templatelinks', 'tl' ); 03382 } 03383 03396 public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03397 global $wgContentHandlerUseDB; 03398 03399 $id = $this->getArticleID(); 03400 03401 # If the page doesn't exist; there can't be any link from this page 03402 if ( !$id ) { 03403 return array(); 03404 } 03405 03406 if ( count( $options ) > 0 ) { 03407 $db = wfGetDB( DB_MASTER ); 03408 } else { 03409 $db = wfGetDB( DB_SLAVE ); 03410 } 03411 03412 $namespaceFiled = "{$prefix}_namespace"; 03413 $titleField = "{$prefix}_title"; 03414 03415 $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ); 03416 if ( $wgContentHandlerUseDB ) { 03417 $fields[] = 'page_content_model'; 03418 } 03419 03420 $res = $db->select( 03421 array( $table, 'page' ), 03422 $fields, 03423 array( "{$prefix}_from" => $id ), 03424 __METHOD__, 03425 $options, 03426 array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) ) 03427 ); 03428 03429 $retVal = array(); 03430 if ( $res->numRows() ) { 03431 $linkCache = LinkCache::singleton(); 03432 foreach ( $res as $row ) { 03433 $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); 03434 if ( $titleObj ) { 03435 if ( $row->page_id ) { 03436 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03437 } else { 03438 $linkCache->addBadLinkObj( $titleObj ); 03439 } 03440 $retVal[] = $titleObj; 03441 } 03442 } 03443 } 03444 return $retVal; 03445 } 03446 03457 public function getTemplateLinksFrom( $options = array() ) { 03458 return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); 03459 } 03460 03467 public function getBrokenLinksFrom() { 03468 if ( $this->getArticleID() == 0 ) { 03469 # All links from article ID 0 are false positives 03470 return array(); 03471 } 03472 03473 $dbr = wfGetDB( DB_SLAVE ); 03474 $res = $dbr->select( 03475 array( 'page', 'pagelinks' ), 03476 array( 'pl_namespace', 'pl_title' ), 03477 array( 03478 'pl_from' => $this->getArticleID(), 03479 'page_namespace IS NULL' 03480 ), 03481 __METHOD__, array(), 03482 array( 03483 'page' => array( 03484 'LEFT JOIN', 03485 array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) 03486 ) 03487 ) 03488 ); 03489 03490 $retVal = array(); 03491 foreach ( $res as $row ) { 03492 $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); 03493 } 03494 return $retVal; 03495 } 03496 03503 public function getSquidURLs() { 03504 $urls = array( 03505 $this->getInternalURL(), 03506 $this->getInternalURL( 'action=history' ) 03507 ); 03508 03509 $pageLang = $this->getPageLanguage(); 03510 if ( $pageLang->hasVariants() ) { 03511 $variants = $pageLang->getVariants(); 03512 foreach ( $variants as $vCode ) { 03513 $urls[] = $this->getInternalURL( '', $vCode ); 03514 } 03515 } 03516 03517 // If we are looking at a css/js user subpage, purge the action=raw. 03518 if ( $this->isJsSubpage() ) { 03519 $urls[] = $this->getInternalUrl( 'action=raw&ctype=text/javascript' ); 03520 } elseif ( $this->isCssSubpage() ) { 03521 $urls[] = $this->getInternalUrl( 'action=raw&ctype=text/css' ); 03522 } 03523 03524 wfRunHooks( 'TitleSquidURLs', array( $this, &$urls ) ); 03525 return $urls; 03526 } 03527 03531 public function purgeSquid() { 03532 global $wgUseSquid; 03533 if ( $wgUseSquid ) { 03534 $urls = $this->getSquidURLs(); 03535 $u = new SquidUpdate( $urls ); 03536 $u->doUpdate(); 03537 } 03538 } 03539 03546 public function moveNoAuth( &$nt ) { 03547 return $this->moveTo( $nt, false ); 03548 } 03549 03560 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { 03561 global $wgUser, $wgContentHandlerUseDB; 03562 03563 $errors = array(); 03564 if ( !$nt ) { 03565 // Normally we'd add this to $errors, but we'll get 03566 // lots of syntax errors if $nt is not an object 03567 return array( array( 'badtitletext' ) ); 03568 } 03569 if ( $this->equals( $nt ) ) { 03570 $errors[] = array( 'selfmove' ); 03571 } 03572 if ( !$this->isMovable() ) { 03573 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 03574 } 03575 if ( $nt->isExternal() ) { 03576 $errors[] = array( 'immobile-target-namespace-iw' ); 03577 } 03578 if ( !$nt->isMovable() ) { 03579 $errors[] = array( 'immobile-target-namespace', $nt->getNsText() ); 03580 } 03581 03582 $oldid = $this->getArticleID(); 03583 $newid = $nt->getArticleID(); 03584 03585 if ( strlen( $nt->getDBkey() ) < 1 ) { 03586 $errors[] = array( 'articleexists' ); 03587 } 03588 if ( 03589 ( $this->getDBkey() == '' ) || 03590 ( !$oldid ) || 03591 ( $nt->getDBkey() == '' ) 03592 ) { 03593 $errors[] = array( 'badarticleerror' ); 03594 } 03595 03596 // Content model checks 03597 if ( !$wgContentHandlerUseDB && 03598 $this->getContentModel() !== $nt->getContentModel() ) { 03599 // can't move a page if that would change the page's content model 03600 $errors[] = array( 03601 'bad-target-model', 03602 ContentHandler::getLocalizedName( $this->getContentModel() ), 03603 ContentHandler::getLocalizedName( $nt->getContentModel() ) 03604 ); 03605 } 03606 03607 // Image-specific checks 03608 if ( $this->getNamespace() == NS_FILE ) { 03609 $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) ); 03610 } 03611 03612 if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) { 03613 $errors[] = array( 'nonfile-cannot-move-to-file' ); 03614 } 03615 03616 if ( $auth ) { 03617 $errors = wfMergeErrorArrays( $errors, 03618 $this->getUserPermissionsErrors( 'move', $wgUser ), 03619 $this->getUserPermissionsErrors( 'edit', $wgUser ), 03620 $nt->getUserPermissionsErrors( 'move-target', $wgUser ), 03621 $nt->getUserPermissionsErrors( 'edit', $wgUser ) ); 03622 } 03623 03624 $match = EditPage::matchSummarySpamRegex( $reason ); 03625 if ( $match !== false ) { 03626 // This is kind of lame, won't display nice 03627 $errors[] = array( 'spamprotectiontext' ); 03628 } 03629 03630 $err = null; 03631 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) { 03632 $errors[] = array( 'hookaborted', $err ); 03633 } 03634 03635 # The move is allowed only if (1) the target doesn't exist, or 03636 # (2) the target is a redirect to the source, and has no history 03637 # (so we can undo bad moves right after they're done). 03638 03639 if ( 0 != $newid ) { # Target exists; check for validity 03640 if ( !$this->isValidMoveTarget( $nt ) ) { 03641 $errors[] = array( 'articleexists' ); 03642 } 03643 } else { 03644 $tp = $nt->getTitleProtection(); 03645 $right = $tp['pt_create_perm']; 03646 if ( $right == 'sysop' ) { 03647 $right = 'editprotected'; // B/C 03648 } 03649 if ( $right == 'autoconfirmed' ) { 03650 $right = 'editsemiprotected'; // B/C 03651 } 03652 if ( $tp and !$wgUser->isAllowed( $right ) ) { 03653 $errors[] = array( 'cantmove-titleprotected' ); 03654 } 03655 } 03656 if ( empty( $errors ) ) { 03657 return true; 03658 } 03659 return $errors; 03660 } 03661 03667 protected function validateFileMoveOperation( $nt ) { 03668 global $wgUser; 03669 03670 $errors = array(); 03671 03672 // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below 03673 03674 $file = wfLocalFile( $this ); 03675 if ( $file->exists() ) { 03676 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { 03677 $errors[] = array( 'imageinvalidfilename' ); 03678 } 03679 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) { 03680 $errors[] = array( 'imagetypemismatch' ); 03681 } 03682 } 03683 03684 if ( $nt->getNamespace() != NS_FILE ) { 03685 $errors[] = array( 'imagenocrossnamespace' ); 03686 // From here we want to do checks on a file object, so if we can't 03687 // create one, we must return. 03688 return $errors; 03689 } 03690 03691 // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here 03692 03693 $destFile = wfLocalFile( $nt ); 03694 if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) { 03695 $errors[] = array( 'file-exists-sharedrepo' ); 03696 } 03697 03698 return $errors; 03699 } 03700 03712 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { 03713 global $wgUser; 03714 $err = $this->isValidMoveOperation( $nt, $auth, $reason ); 03715 if ( is_array( $err ) ) { 03716 // Auto-block user's IP if the account was "hard" blocked 03717 $wgUser->spreadAnyEditBlock(); 03718 return $err; 03719 } 03720 // Check suppressredirect permission 03721 if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) { 03722 $createRedirect = true; 03723 } 03724 03725 wfRunHooks( 'TitleMove', array( $this, $nt, $wgUser ) ); 03726 03727 // If it is a file, move it first. 03728 // It is done before all other moving stuff is done because it's hard to revert. 03729 $dbw = wfGetDB( DB_MASTER ); 03730 if ( $this->getNamespace() == NS_FILE ) { 03731 $file = wfLocalFile( $this ); 03732 if ( $file->exists() ) { 03733 $status = $file->move( $nt ); 03734 if ( !$status->isOk() ) { 03735 return $status->getErrorsArray(); 03736 } 03737 } 03738 // Clear RepoGroup process cache 03739 RepoGroup::singleton()->clearCache( $this ); 03740 RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache 03741 } 03742 03743 $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own. 03744 $pageid = $this->getArticleID( self::GAID_FOR_UPDATE ); 03745 $protected = $this->isProtected(); 03746 03747 // Do the actual move 03748 $this->moveToInternal( $nt, $reason, $createRedirect ); 03749 03750 // Refresh the sortkey for this row. Be careful to avoid resetting 03751 // cl_timestamp, which may disturb time-based lists on some sites. 03752 $prefixes = $dbw->select( 03753 'categorylinks', 03754 array( 'cl_sortkey_prefix', 'cl_to' ), 03755 array( 'cl_from' => $pageid ), 03756 __METHOD__ 03757 ); 03758 foreach ( $prefixes as $prefixRow ) { 03759 $prefix = $prefixRow->cl_sortkey_prefix; 03760 $catTo = $prefixRow->cl_to; 03761 $dbw->update( 'categorylinks', 03762 array( 03763 'cl_sortkey' => Collation::singleton()->getSortKey( 03764 $nt->getCategorySortkey( $prefix ) ), 03765 'cl_timestamp=cl_timestamp' ), 03766 array( 03767 'cl_from' => $pageid, 03768 'cl_to' => $catTo ), 03769 __METHOD__ 03770 ); 03771 } 03772 03773 $redirid = $this->getArticleID(); 03774 03775 if ( $protected ) { 03776 # Protect the redirect title as the title used to be... 03777 $dbw->insertSelect( 'page_restrictions', 'page_restrictions', 03778 array( 03779 'pr_page' => $redirid, 03780 'pr_type' => 'pr_type', 03781 'pr_level' => 'pr_level', 03782 'pr_cascade' => 'pr_cascade', 03783 'pr_user' => 'pr_user', 03784 'pr_expiry' => 'pr_expiry' 03785 ), 03786 array( 'pr_page' => $pageid ), 03787 __METHOD__, 03788 array( 'IGNORE' ) 03789 ); 03790 # Update the protection log 03791 $log = new LogPage( 'protect' ); 03792 $comment = wfMessage( 03793 'prot_1movedto2', 03794 $this->getPrefixedText(), 03795 $nt->getPrefixedText() 03796 )->inContentLanguage()->text(); 03797 if ( $reason ) { 03798 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03799 } 03800 // @todo FIXME: $params? 03801 $logId = $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ), $wgUser ); 03802 03803 // reread inserted pr_ids for log relation 03804 $insertedPrIds = $dbw->select( 03805 'page_restrictions', 03806 'pr_id', 03807 array( 'pr_page' => $redirid ), 03808 __METHOD__ 03809 ); 03810 $logRelationsValues = array(); 03811 foreach ( $insertedPrIds as $prid ) { 03812 $logRelationsValues[] = $prid->pr_id; 03813 } 03814 $log->addRelations( 'pr_id', $logRelationsValues, $logId ); 03815 } 03816 03817 # Update watchlists 03818 $oldnamespace = MWNamespace::getSubject( $this->getNamespace() ); 03819 $newnamespace = MWNamespace::getSubject( $nt->getNamespace() ); 03820 $oldtitle = $this->getDBkey(); 03821 $newtitle = $nt->getDBkey(); 03822 03823 if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) { 03824 WatchedItem::duplicateEntries( $this, $nt ); 03825 } 03826 03827 $dbw->commit( __METHOD__ ); 03828 03829 wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid, $reason ) ); 03830 return true; 03831 } 03832 03843 private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) { 03844 global $wgUser, $wgContLang; 03845 03846 if ( $nt->exists() ) { 03847 $moveOverRedirect = true; 03848 $logType = 'move_redir'; 03849 } else { 03850 $moveOverRedirect = false; 03851 $logType = 'move'; 03852 } 03853 03854 if ( $createRedirect ) { 03855 $contentHandler = ContentHandler::getForTitle( $this ); 03856 $redirectContent = $contentHandler->makeRedirectContent( $nt, 03857 wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() ); 03858 03859 // NOTE: If this page's content model does not support redirects, $redirectContent will be null. 03860 } else { 03861 $redirectContent = null; 03862 } 03863 03864 $logEntry = new ManualLogEntry( 'move', $logType ); 03865 $logEntry->setPerformer( $wgUser ); 03866 $logEntry->setTarget( $this ); 03867 $logEntry->setComment( $reason ); 03868 $logEntry->setParameters( array( 03869 '4::target' => $nt->getPrefixedText(), 03870 '5::noredir' => $redirectContent ? '0': '1', 03871 ) ); 03872 03873 $formatter = LogFormatter::newFromEntry( $logEntry ); 03874 $formatter->setContext( RequestContext::newExtraneousContext( $this ) ); 03875 $comment = $formatter->getPlainActionText(); 03876 if ( $reason ) { 03877 $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 03878 } 03879 # Truncate for whole multibyte characters. 03880 $comment = $wgContLang->truncate( $comment, 255 ); 03881 03882 $oldid = $this->getArticleID(); 03883 03884 $dbw = wfGetDB( DB_MASTER ); 03885 03886 $newpage = WikiPage::factory( $nt ); 03887 03888 if ( $moveOverRedirect ) { 03889 $newid = $nt->getArticleID(); 03890 $newcontent = $newpage->getContent(); 03891 03892 # Delete the old redirect. We don't save it to history since 03893 # by definition if we've got here it's rather uninteresting. 03894 # We have to remove it so that the next step doesn't trigger 03895 # a conflict on the unique namespace+title index... 03896 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ ); 03897 03898 $newpage->doDeleteUpdates( $newid, $newcontent ); 03899 } 03900 03901 # Save a null revision in the page's history notifying of the move 03902 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); 03903 if ( !is_object( $nullRevision ) ) { 03904 throw new MWException( 'No valid null revision produced in ' . __METHOD__ ); 03905 } 03906 03907 $nullRevision->insertOn( $dbw ); 03908 03909 # Change the name of the target page: 03910 $dbw->update( 'page', 03911 /* SET */ array( 03912 'page_namespace' => $nt->getNamespace(), 03913 'page_title' => $nt->getDBkey(), 03914 ), 03915 /* WHERE */ array( 'page_id' => $oldid ), 03916 __METHOD__ 03917 ); 03918 03919 // clean up the old title before reset article id - bug 45348 03920 if ( !$redirectContent ) { 03921 WikiPage::onArticleDelete( $this ); 03922 } 03923 03924 $this->resetArticleID( 0 ); // 0 == non existing 03925 $nt->resetArticleID( $oldid ); 03926 $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397 03927 03928 $newpage->updateRevisionOn( $dbw, $nullRevision ); 03929 03930 wfRunHooks( 'NewRevisionFromEditComplete', 03931 array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) ); 03932 03933 $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) ); 03934 03935 if ( !$moveOverRedirect ) { 03936 WikiPage::onArticleCreate( $nt ); 03937 } 03938 03939 # Recreate the redirect, this time in the other direction. 03940 if ( $redirectContent ) { 03941 $redirectArticle = WikiPage::factory( $this ); 03942 $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // bug 46397 03943 $newid = $redirectArticle->insertOn( $dbw ); 03944 if ( $newid ) { // sanity 03945 $this->resetArticleID( $newid ); 03946 $redirectRevision = new Revision( array( 03947 'title' => $this, // for determining the default content model 03948 'page' => $newid, 03949 'comment' => $comment, 03950 'content' => $redirectContent ) ); 03951 $redirectRevision->insertOn( $dbw ); 03952 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); 03953 03954 wfRunHooks( 'NewRevisionFromEditComplete', 03955 array( $redirectArticle, $redirectRevision, false, $wgUser ) ); 03956 03957 $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) ); 03958 } 03959 } 03960 03961 # Log the move 03962 $logid = $logEntry->insert(); 03963 $logEntry->publish( $logid ); 03964 } 03965 03978 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { 03979 global $wgMaximumMovedPages; 03980 // Check permissions 03981 if ( !$this->userCan( 'move-subpages' ) ) { 03982 return array( 'cant-move-subpages' ); 03983 } 03984 // Do the source and target namespaces support subpages? 03985 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03986 return array( 'namespace-nosubpages', 03987 MWNamespace::getCanonicalName( $this->getNamespace() ) ); 03988 } 03989 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) { 03990 return array( 'namespace-nosubpages', 03991 MWNamespace::getCanonicalName( $nt->getNamespace() ) ); 03992 } 03993 03994 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 ); 03995 $retval = array(); 03996 $count = 0; 03997 foreach ( $subpages as $oldSubpage ) { 03998 $count++; 03999 if ( $count > $wgMaximumMovedPages ) { 04000 $retval[$oldSubpage->getPrefixedText()] = 04001 array( 'movepage-max-pages', 04002 $wgMaximumMovedPages ); 04003 break; 04004 } 04005 04006 // We don't know whether this function was called before 04007 // or after moving the root page, so check both 04008 // $this and $nt 04009 if ( $oldSubpage->getArticleID() == $this->getArticleID() 04010 || $oldSubpage->getArticleID() == $nt->getArticleID() 04011 ) { 04012 // When moving a page to a subpage of itself, 04013 // don't move it twice 04014 continue; 04015 } 04016 $newPageName = preg_replace( 04017 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#', 04018 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 04019 $oldSubpage->getDBkey() ); 04020 if ( $oldSubpage->isTalkPage() ) { 04021 $newNs = $nt->getTalkPage()->getNamespace(); 04022 } else { 04023 $newNs = $nt->getSubjectPage()->getNamespace(); 04024 } 04025 # Bug 14385: we need makeTitleSafe because the new page names may 04026 # be longer than 255 characters. 04027 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); 04028 04029 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); 04030 if ( $success === true ) { 04031 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); 04032 } else { 04033 $retval[$oldSubpage->getPrefixedText()] = $success; 04034 } 04035 } 04036 return $retval; 04037 } 04038 04045 public function isSingleRevRedirect() { 04046 global $wgContentHandlerUseDB; 04047 04048 $dbw = wfGetDB( DB_MASTER ); 04049 04050 # Is it a redirect? 04051 $fields = array( 'page_is_redirect', 'page_latest', 'page_id' ); 04052 if ( $wgContentHandlerUseDB ) { 04053 $fields[] = 'page_content_model'; 04054 } 04055 04056 $row = $dbw->selectRow( 'page', 04057 $fields, 04058 $this->pageCond(), 04059 __METHOD__, 04060 array( 'FOR UPDATE' ) 04061 ); 04062 # Cache some fields we may want 04063 $this->mArticleID = $row ? intval( $row->page_id ) : 0; 04064 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; 04065 $this->mLatestID = $row ? intval( $row->page_latest ) : false; 04066 $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false; 04067 if ( !$this->mRedirect ) { 04068 return false; 04069 } 04070 # Does the article have a history? 04071 $row = $dbw->selectField( array( 'page', 'revision' ), 04072 'rev_id', 04073 array( 'page_namespace' => $this->getNamespace(), 04074 'page_title' => $this->getDBkey(), 04075 'page_id=rev_page', 04076 'page_latest != rev_id' 04077 ), 04078 __METHOD__, 04079 array( 'FOR UPDATE' ) 04080 ); 04081 # Return true if there was no history 04082 return ( $row === false ); 04083 } 04084 04092 public function isValidMoveTarget( $nt ) { 04093 # Is it an existing file? 04094 if ( $nt->getNamespace() == NS_FILE ) { 04095 $file = wfLocalFile( $nt ); 04096 if ( $file->exists() ) { 04097 wfDebug( __METHOD__ . ": file exists\n" ); 04098 return false; 04099 } 04100 } 04101 # Is it a redirect with no history? 04102 if ( !$nt->isSingleRevRedirect() ) { 04103 wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); 04104 return false; 04105 } 04106 # Get the article text 04107 $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST ); 04108 if ( !is_object( $rev ) ) { 04109 return false; 04110 } 04111 $content = $rev->getContent(); 04112 # Does the redirect point to the source? 04113 # Or is it a broken self-redirect, usually caused by namespace collisions? 04114 $redirTitle = $content ? $content->getRedirectTarget() : null; 04115 04116 if ( $redirTitle ) { 04117 if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && 04118 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) { 04119 wfDebug( __METHOD__ . ": redirect points to other page\n" ); 04120 return false; 04121 } else { 04122 return true; 04123 } 04124 } else { 04125 # Fail safe (not a redirect after all. strange.) 04126 wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . 04127 " is a redirect, but it doesn't contain a valid redirect.\n" ); 04128 return false; 04129 } 04130 } 04131 04139 public function getParentCategories() { 04140 global $wgContLang; 04141 04142 $data = array(); 04143 04144 $titleKey = $this->getArticleID(); 04145 04146 if ( $titleKey === 0 ) { 04147 return $data; 04148 } 04149 04150 $dbr = wfGetDB( DB_SLAVE ); 04151 04152 $res = $dbr->select( 04153 'categorylinks', 04154 'cl_to', 04155 array( 'cl_from' => $titleKey ), 04156 __METHOD__ 04157 ); 04158 04159 if ( $res->numRows() > 0 ) { 04160 foreach ( $res as $row ) { 04161 // $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to); 04162 $data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText(); 04163 } 04164 } 04165 return $data; 04166 } 04167 04174 public function getParentCategoryTree( $children = array() ) { 04175 $stack = array(); 04176 $parents = $this->getParentCategories(); 04177 04178 if ( $parents ) { 04179 foreach ( $parents as $parent => $current ) { 04180 if ( array_key_exists( $parent, $children ) ) { 04181 # Circular reference 04182 $stack[$parent] = array(); 04183 } else { 04184 $nt = Title::newFromText( $parent ); 04185 if ( $nt ) { 04186 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) ); 04187 } 04188 } 04189 } 04190 } 04191 04192 return $stack; 04193 } 04194 04201 public function pageCond() { 04202 if ( $this->mArticleID > 0 ) { 04203 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs 04204 return array( 'page_id' => $this->mArticleID ); 04205 } else { 04206 return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); 04207 } 04208 } 04209 04217 public function getPreviousRevisionID( $revId, $flags = 0 ) { 04218 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04219 $revId = $db->selectField( 'revision', 'rev_id', 04220 array( 04221 'rev_page' => $this->getArticleID( $flags ), 04222 'rev_id < ' . intval( $revId ) 04223 ), 04224 __METHOD__, 04225 array( 'ORDER BY' => 'rev_id DESC' ) 04226 ); 04227 04228 if ( $revId === false ) { 04229 return false; 04230 } else { 04231 return intval( $revId ); 04232 } 04233 } 04234 04242 public function getNextRevisionID( $revId, $flags = 0 ) { 04243 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04244 $revId = $db->selectField( 'revision', 'rev_id', 04245 array( 04246 'rev_page' => $this->getArticleID( $flags ), 04247 'rev_id > ' . intval( $revId ) 04248 ), 04249 __METHOD__, 04250 array( 'ORDER BY' => 'rev_id' ) 04251 ); 04252 04253 if ( $revId === false ) { 04254 return false; 04255 } else { 04256 return intval( $revId ); 04257 } 04258 } 04259 04266 public function getFirstRevision( $flags = 0 ) { 04267 $pageId = $this->getArticleID( $flags ); 04268 if ( $pageId ) { 04269 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04270 $row = $db->selectRow( 'revision', Revision::selectFields(), 04271 array( 'rev_page' => $pageId ), 04272 __METHOD__, 04273 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) 04274 ); 04275 if ( $row ) { 04276 return new Revision( $row ); 04277 } 04278 } 04279 return null; 04280 } 04281 04288 public function getEarliestRevTime( $flags = 0 ) { 04289 $rev = $this->getFirstRevision( $flags ); 04290 return $rev ? $rev->getTimestamp() : null; 04291 } 04292 04298 public function isNewPage() { 04299 $dbr = wfGetDB( DB_SLAVE ); 04300 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); 04301 } 04302 04308 public function isBigDeletion() { 04309 global $wgDeleteRevisionsLimit; 04310 04311 if ( !$wgDeleteRevisionsLimit ) { 04312 return false; 04313 } 04314 04315 $revCount = $this->estimateRevisionCount(); 04316 return $revCount > $wgDeleteRevisionsLimit; 04317 } 04318 04324 public function estimateRevisionCount() { 04325 if ( !$this->exists() ) { 04326 return 0; 04327 } 04328 04329 if ( $this->mEstimateRevisions === null ) { 04330 $dbr = wfGetDB( DB_SLAVE ); 04331 $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', 04332 array( 'rev_page' => $this->getArticleID() ), __METHOD__ ); 04333 } 04334 04335 return $this->mEstimateRevisions; 04336 } 04337 04347 public function countRevisionsBetween( $old, $new, $max = null ) { 04348 if ( !( $old instanceof Revision ) ) { 04349 $old = Revision::newFromTitle( $this, (int)$old ); 04350 } 04351 if ( !( $new instanceof Revision ) ) { 04352 $new = Revision::newFromTitle( $this, (int)$new ); 04353 } 04354 if ( !$old || !$new ) { 04355 return 0; // nothing to compare 04356 } 04357 $dbr = wfGetDB( DB_SLAVE ); 04358 $conds = array( 04359 'rev_page' => $this->getArticleID(), 04360 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04361 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04362 ); 04363 if ( $max !== null ) { 04364 $res = $dbr->select( 'revision', '1', 04365 $conds, 04366 __METHOD__, 04367 array( 'LIMIT' => $max + 1 ) // extra to detect truncation 04368 ); 04369 return $res->numRows(); 04370 } else { 04371 return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ ); 04372 } 04373 } 04374 04391 public function getAuthorsBetween( $old, $new, $limit, $options = array() ) { 04392 if ( !( $old instanceof Revision ) ) { 04393 $old = Revision::newFromTitle( $this, (int)$old ); 04394 } 04395 if ( !( $new instanceof Revision ) ) { 04396 $new = Revision::newFromTitle( $this, (int)$new ); 04397 } 04398 // XXX: what if Revision objects are passed in, but they don't refer to this title? 04399 // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID() 04400 // in the sanity check below? 04401 if ( !$old || !$new ) { 04402 return null; // nothing to compare 04403 } 04404 $authors = array(); 04405 $old_cmp = '>'; 04406 $new_cmp = '<'; 04407 $options = (array)$options; 04408 if ( in_array( 'include_old', $options ) ) { 04409 $old_cmp = '>='; 04410 } 04411 if ( in_array( 'include_new', $options ) ) { 04412 $new_cmp = '<='; 04413 } 04414 if ( in_array( 'include_both', $options ) ) { 04415 $old_cmp = '>='; 04416 $new_cmp = '<='; 04417 } 04418 // No DB query needed if $old and $new are the same or successive revisions: 04419 if ( $old->getId() === $new->getId() ) { 04420 return ( $old_cmp === '>' && $new_cmp === '<' ) ? array() : array( $old->getRawUserText() ); 04421 } elseif ( $old->getId() === $new->getParentId() ) { 04422 if ( $old_cmp === '>=' && $new_cmp === '<=' ) { 04423 $authors[] = $old->getRawUserText(); 04424 if ( $old->getRawUserText() != $new->getRawUserText() ) { 04425 $authors[] = $new->getRawUserText(); 04426 } 04427 } elseif ( $old_cmp === '>=' ) { 04428 $authors[] = $old->getRawUserText(); 04429 } elseif ( $new_cmp === '<=' ) { 04430 $authors[] = $new->getRawUserText(); 04431 } 04432 return $authors; 04433 } 04434 $dbr = wfGetDB( DB_SLAVE ); 04435 $res = $dbr->select( 'revision', 'DISTINCT rev_user_text', 04436 array( 04437 'rev_page' => $this->getArticleID(), 04438 "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04439 "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04440 ), __METHOD__, 04441 array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated 04442 ); 04443 foreach ( $res as $row ) { 04444 $authors[] = $row->rev_user_text; 04445 } 04446 return $authors; 04447 } 04448 04463 public function countAuthorsBetween( $old, $new, $limit, $options = array() ) { 04464 $authors = $this->getAuthorsBetween( $old, $new, $limit, $options ); 04465 return $authors ? count( $authors ) : 0; 04466 } 04467 04474 public function equals( Title $title ) { 04475 // Note: === is necessary for proper matching of number-like titles. 04476 return $this->getInterwiki() === $title->getInterwiki() 04477 && $this->getNamespace() == $title->getNamespace() 04478 && $this->getDBkey() === $title->getDBkey(); 04479 } 04480 04487 public function isSubpageOf( Title $title ) { 04488 return $this->getInterwiki() === $title->getInterwiki() 04489 && $this->getNamespace() == $title->getNamespace() 04490 && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; 04491 } 04492 04502 public function exists() { 04503 return $this->getArticleID() != 0; 04504 } 04505 04522 public function isAlwaysKnown() { 04523 $isKnown = null; 04524 04535 wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) ); 04536 04537 if ( !is_null( $isKnown ) ) { 04538 return $isKnown; 04539 } 04540 04541 if ( $this->isExternal() ) { 04542 return true; // any interwiki link might be viewable, for all we know 04543 } 04544 04545 switch ( $this->mNamespace ) { 04546 case NS_MEDIA: 04547 case NS_FILE: 04548 // file exists, possibly in a foreign repo 04549 return (bool)wfFindFile( $this ); 04550 case NS_SPECIAL: 04551 // valid special page 04552 return SpecialPageFactory::exists( $this->getDBkey() ); 04553 case NS_MAIN: 04554 // selflink, possibly with fragment 04555 return $this->mDbkeyform == ''; 04556 case NS_MEDIAWIKI: 04557 // known system message 04558 return $this->hasSourceText() !== false; 04559 default: 04560 return false; 04561 } 04562 } 04563 04575 public function isKnown() { 04576 return $this->isAlwaysKnown() || $this->exists(); 04577 } 04578 04584 public function hasSourceText() { 04585 if ( $this->exists() ) { 04586 return true; 04587 } 04588 04589 if ( $this->mNamespace == NS_MEDIAWIKI ) { 04590 // If the page doesn't exist but is a known system message, default 04591 // message content will be displayed, same for language subpages- 04592 // Use always content language to avoid loading hundreds of languages 04593 // to get the link color. 04594 global $wgContLang; 04595 list( $name, ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04596 $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false ); 04597 return $message->exists(); 04598 } 04599 04600 return false; 04601 } 04602 04608 public function getDefaultMessageText() { 04609 global $wgContLang; 04610 04611 if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case 04612 return false; 04613 } 04614 04615 list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04616 $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false ); 04617 04618 if ( $message->exists() ) { 04619 return $message->plain(); 04620 } else { 04621 return false; 04622 } 04623 } 04624 04630 public function invalidateCache() { 04631 if ( wfReadOnly() ) { 04632 return false; 04633 } 04634 04635 $method = __METHOD__; 04636 $dbw = wfGetDB( DB_MASTER ); 04637 $conds = $this->pageCond(); 04638 $dbw->onTransactionIdle( function() use ( $dbw, $conds, $method ) { 04639 $dbw->update( 04640 'page', 04641 array( 'page_touched' => $dbw->timestamp() ), 04642 $conds, 04643 $method 04644 ); 04645 } ); 04646 04647 return true; 04648 } 04649 04655 public function touchLinks() { 04656 $u = new HTMLCacheUpdate( $this, 'pagelinks' ); 04657 $u->doUpdate(); 04658 04659 if ( $this->getNamespace() == NS_CATEGORY ) { 04660 $u = new HTMLCacheUpdate( $this, 'categorylinks' ); 04661 $u->doUpdate(); 04662 } 04663 } 04664 04671 public function getTouched( $db = null ) { 04672 $db = isset( $db ) ? $db : wfGetDB( DB_SLAVE ); 04673 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); 04674 return $touched; 04675 } 04676 04683 public function getNotificationTimestamp( $user = null ) { 04684 global $wgUser, $wgShowUpdatedMarker; 04685 // Assume current user if none given 04686 if ( !$user ) { 04687 $user = $wgUser; 04688 } 04689 // Check cache first 04690 $uid = $user->getId(); 04691 // avoid isset here, as it'll return false for null entries 04692 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) { 04693 return $this->mNotificationTimestamp[$uid]; 04694 } 04695 if ( !$uid || !$wgShowUpdatedMarker || !$user->isAllowed( 'viewmywatchlist' ) ) { 04696 $this->mNotificationTimestamp[$uid] = false; 04697 return $this->mNotificationTimestamp[$uid]; 04698 } 04699 // Don't cache too much! 04700 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) { 04701 $this->mNotificationTimestamp = array(); 04702 } 04703 $dbr = wfGetDB( DB_SLAVE ); 04704 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', 04705 'wl_notificationtimestamp', 04706 array( 04707 'wl_user' => $user->getId(), 04708 'wl_namespace' => $this->getNamespace(), 04709 'wl_title' => $this->getDBkey(), 04710 ), 04711 __METHOD__ 04712 ); 04713 return $this->mNotificationTimestamp[$uid]; 04714 } 04715 04722 public function getNamespaceKey( $prepend = 'nstab-' ) { 04723 global $wgContLang; 04724 // Gets the subject namespace if this title 04725 $namespace = MWNamespace::getSubject( $this->getNamespace() ); 04726 // Checks if canonical namespace name exists for namespace 04727 if ( MWNamespace::exists( $this->getNamespace() ) ) { 04728 // Uses canonical namespace name 04729 $namespaceKey = MWNamespace::getCanonicalName( $namespace ); 04730 } else { 04731 // Uses text of namespace 04732 $namespaceKey = $this->getSubjectNsText(); 04733 } 04734 // Makes namespace key lowercase 04735 $namespaceKey = $wgContLang->lc( $namespaceKey ); 04736 // Uses main 04737 if ( $namespaceKey == '' ) { 04738 $namespaceKey = 'main'; 04739 } 04740 // Changes file to image for backwards compatibility 04741 if ( $namespaceKey == 'file' ) { 04742 $namespaceKey = 'image'; 04743 } 04744 return $prepend . $namespaceKey; 04745 } 04746 04753 public function getRedirectsHere( $ns = null ) { 04754 $redirs = array(); 04755 04756 $dbr = wfGetDB( DB_SLAVE ); 04757 $where = array( 04758 'rd_namespace' => $this->getNamespace(), 04759 'rd_title' => $this->getDBkey(), 04760 'rd_from = page_id' 04761 ); 04762 if ( $this->isExternal() ) { 04763 $where['rd_interwiki'] = $this->getInterwiki(); 04764 } else { 04765 $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'; 04766 } 04767 if ( !is_null( $ns ) ) { 04768 $where['page_namespace'] = $ns; 04769 } 04770 04771 $res = $dbr->select( 04772 array( 'redirect', 'page' ), 04773 array( 'page_namespace', 'page_title' ), 04774 $where, 04775 __METHOD__ 04776 ); 04777 04778 foreach ( $res as $row ) { 04779 $redirs[] = self::newFromRow( $row ); 04780 } 04781 return $redirs; 04782 } 04783 04789 public function isValidRedirectTarget() { 04790 global $wgInvalidRedirectTargets; 04791 04792 // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here 04793 if ( $this->isSpecial( 'Userlogout' ) ) { 04794 return false; 04795 } 04796 04797 foreach ( $wgInvalidRedirectTargets as $target ) { 04798 if ( $this->isSpecial( $target ) ) { 04799 return false; 04800 } 04801 } 04802 04803 return true; 04804 } 04805 04811 public function getBacklinkCache() { 04812 return BacklinkCache::get( $this ); 04813 } 04814 04820 public function canUseNoindex() { 04821 global $wgContentNamespaces, $wgExemptFromUserRobotsControl; 04822 04823 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl ) 04824 ? $wgContentNamespaces 04825 : $wgExemptFromUserRobotsControl; 04826 04827 return !in_array( $this->mNamespace, $bannedNamespaces ); 04828 04829 } 04830 04841 public function getCategorySortkey( $prefix = '' ) { 04842 $unprefixed = $this->getText(); 04843 04844 // Anything that uses this hook should only depend 04845 // on the Title object passed in, and should probably 04846 // tell the users to run updateCollations.php --force 04847 // in order to re-sort existing category relations. 04848 wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) ); 04849 if ( $prefix !== '' ) { 04850 # Separate with a line feed, so the unprefixed part is only used as 04851 # a tiebreaker when two pages have the exact same prefix. 04852 # In UCA, tab is the only character that can sort above LF 04853 # so we strip both of them from the original prefix. 04854 $prefix = strtr( $prefix, "\n\t", ' ' ); 04855 return "$prefix\n$unprefixed"; 04856 } 04857 return $unprefixed; 04858 } 04859 04868 public function getPageLanguage() { 04869 global $wgLang, $wgLanguageCode; 04870 wfProfileIn( __METHOD__ ); 04871 if ( $this->isSpecialPage() ) { 04872 // special pages are in the user language 04873 wfProfileOut( __METHOD__ ); 04874 return $wgLang; 04875 } 04876 04877 if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) { 04878 // Note that this may depend on user settings, so the cache should be only per-request. 04879 // NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language! 04880 // Checking $wgLanguageCode hasn't changed for the benefit of unit tests. 04881 $contentHandler = ContentHandler::getForTitle( $this ); 04882 $langObj = wfGetLangObj( $contentHandler->getPageLanguage( $this ) ); 04883 $this->mPageLanguage = array( $langObj->getCode(), $wgLanguageCode ); 04884 } else { 04885 $langObj = wfGetLangObj( $this->mPageLanguage[0] ); 04886 } 04887 wfProfileOut( __METHOD__ ); 04888 return $langObj; 04889 } 04890 04899 public function getPageViewLanguage() { 04900 global $wgLang; 04901 04902 if ( $this->isSpecialPage() ) { 04903 // If the user chooses a variant, the content is actually 04904 // in a language whose code is the variant code. 04905 $variant = $wgLang->getPreferredVariant(); 04906 if ( $wgLang->getCode() !== $variant ) { 04907 return Language::factory( $variant ); 04908 } 04909 04910 return $wgLang; 04911 } 04912 04913 //NOTE: can't be cached persistently, depends on user settings 04914 //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language! 04915 $contentHandler = ContentHandler::getForTitle( $this ); 04916 $pageLang = $contentHandler->getPageViewLanguage( $this ); 04917 return $pageLang; 04918 } 04919 04930 public function getEditNotices( $oldid = 0 ) { 04931 $notices = array(); 04932 04933 # Optional notices on a per-namespace and per-page basis 04934 $editnotice_ns = 'editnotice-' . $this->getNamespace(); 04935 $editnotice_ns_message = wfMessage( $editnotice_ns ); 04936 if ( $editnotice_ns_message->exists() ) { 04937 $notices[$editnotice_ns] = $editnotice_ns_message->parseAsBlock(); 04938 } 04939 if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) { 04940 $parts = explode( '/', $this->getDBkey() ); 04941 $editnotice_base = $editnotice_ns; 04942 while ( count( $parts ) > 0 ) { 04943 $editnotice_base .= '-' . array_shift( $parts ); 04944 $editnotice_base_msg = wfMessage( $editnotice_base ); 04945 if ( $editnotice_base_msg->exists() ) { 04946 $notices[$editnotice_base] = $editnotice_base_msg->parseAsBlock(); 04947 } 04948 } 04949 } else { 04950 # Even if there are no subpages in namespace, we still don't want / in MW ns. 04951 $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() ); 04952 $editnoticeMsg = wfMessage( $editnoticeText ); 04953 if ( $editnoticeMsg->exists() ) { 04954 $notices[$editnoticeText] = $editnoticeMsg->parseAsBlock(); 04955 } 04956 } 04957 04958 wfRunHooks( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) ); 04959 return $notices; 04960 } 04961 }