MediaWiki
REL1_24
|
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 00060 public $mTextform = ''; 00061 00063 public $mUrlform = ''; 00064 00066 public $mDbkeyform = ''; 00067 00069 protected $mUserCaseDBKey; 00070 00072 public $mNamespace = NS_MAIN; 00073 00075 public $mInterwiki = ''; 00076 00078 private $mLocalInterwiki = false; 00079 00081 public $mFragment = ''; 00082 00084 public $mArticleID = -1; 00085 00087 protected $mLatestID = false; 00088 00093 public $mContentModel = false; 00094 00096 private $mEstimateRevisions; 00097 00099 public $mRestrictions = array(); 00100 00102 protected $mOldRestrictions = false; 00103 00105 public $mCascadeRestriction; 00106 00108 public $mCascadingRestrictions; 00109 00111 protected $mRestrictionsExpiry = array(); 00112 00114 protected $mHasCascadingRestrictions; 00115 00117 public $mCascadeSources; 00118 00120 public $mRestrictionsLoaded = false; 00121 00123 protected $mPrefixedText = null; 00124 00126 public $mTitleProtection; 00127 00133 public $mDefaultNamespace = NS_MAIN; 00134 00139 protected $mWatched = null; 00140 00142 protected $mLength = -1; 00143 00145 public $mRedirect = null; 00146 00148 private $mNotificationTimestamp = array(); 00149 00151 private $mHasSubpages; 00152 00154 private $mPageLanguage = false; 00155 00157 private $mDbPageLanguage = null; 00158 00160 private $mTitleValue = null; 00161 00163 private $mIsBigDeletion = null; 00164 // @} 00165 00174 private static function getTitleParser() { 00175 global $wgContLang, $wgLocalInterwikis; 00176 00177 static $titleCodec = null; 00178 static $titleCodecFingerprint = null; 00179 00180 // $wgContLang and $wgLocalInterwikis may change (especially while testing), 00181 // make sure we are using the right one. To detect changes over the course 00182 // of a request, we remember a fingerprint of the config used to create the 00183 // codec singleton, and re-create it if the fingerprint doesn't match. 00184 $fingerprint = spl_object_hash( $wgContLang ) . '|' . join( '+', $wgLocalInterwikis ); 00185 00186 if ( $fingerprint !== $titleCodecFingerprint ) { 00187 $titleCodec = null; 00188 } 00189 00190 if ( !$titleCodec ) { 00191 $titleCodec = new MediaWikiTitleCodec( 00192 $wgContLang, 00193 GenderCache::singleton(), 00194 $wgLocalInterwikis 00195 ); 00196 $titleCodecFingerprint = $fingerprint; 00197 } 00198 00199 return $titleCodec; 00200 } 00201 00210 private static function getTitleFormatter() { 00211 //NOTE: we know that getTitleParser() returns a MediaWikiTitleCodec, 00212 // which implements TitleFormatter. 00213 return self::getTitleParser(); 00214 } 00215 00216 function __construct() { 00217 } 00218 00227 public static function newFromDBkey( $key ) { 00228 $t = new Title(); 00229 $t->mDbkeyform = $key; 00230 if ( $t->secureAndSplit() ) { 00231 return $t; 00232 } else { 00233 return null; 00234 } 00235 } 00236 00244 public static function newFromTitleValue( TitleValue $titleValue ) { 00245 return self::makeTitle( 00246 $titleValue->getNamespace(), 00247 $titleValue->getText(), 00248 $titleValue->getFragment() ); 00249 } 00250 00264 public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { 00265 if ( is_object( $text ) ) { 00266 throw new MWException( 'Title::newFromText given an object' ); 00267 } 00268 00269 $cache = self::getTitleCache(); 00270 00279 if ( $defaultNamespace == NS_MAIN && $cache->has( $text ) ) { 00280 return $cache->get( $text ); 00281 } 00282 00283 # Convert things like é ā or 〗 into normalized (bug 14952) text 00284 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); 00285 00286 $t = new Title(); 00287 $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); 00288 $t->mDefaultNamespace = intval( $defaultNamespace ); 00289 00290 if ( $t->secureAndSplit() ) { 00291 if ( $defaultNamespace == NS_MAIN ) { 00292 $cache->set( $text, $t ); 00293 } 00294 return $t; 00295 } else { 00296 return null; 00297 } 00298 } 00299 00315 public static function newFromURL( $url ) { 00316 $t = new Title(); 00317 00318 # For compatibility with old buggy URLs. "+" is usually not valid in titles, 00319 # but some URLs used it as a space replacement and they still come 00320 # from some external search tools. 00321 if ( strpos( self::legalChars(), '+' ) === false ) { 00322 $url = str_replace( '+', ' ', $url ); 00323 } 00324 00325 $t->mDbkeyform = str_replace( ' ', '_', $url ); 00326 if ( $t->secureAndSplit() ) { 00327 return $t; 00328 } else { 00329 return null; 00330 } 00331 } 00332 00336 private static function getTitleCache() { 00337 if ( self::$titleCache == null ) { 00338 self::$titleCache = new MapCacheLRU( self::CACHE_MAX ); 00339 } 00340 return self::$titleCache; 00341 } 00342 00350 protected static function getSelectFields() { 00351 global $wgContentHandlerUseDB; 00352 00353 $fields = array( 00354 'page_namespace', 'page_title', 'page_id', 00355 'page_len', 'page_is_redirect', 'page_latest', 00356 ); 00357 00358 if ( $wgContentHandlerUseDB ) { 00359 $fields[] = 'page_content_model'; 00360 } 00361 00362 return $fields; 00363 } 00364 00372 public static function newFromID( $id, $flags = 0 ) { 00373 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 00374 $row = $db->selectRow( 00375 'page', 00376 self::getSelectFields(), 00377 array( 'page_id' => $id ), 00378 __METHOD__ 00379 ); 00380 if ( $row !== false ) { 00381 $title = Title::newFromRow( $row ); 00382 } else { 00383 $title = null; 00384 } 00385 return $title; 00386 } 00387 00394 public static function newFromIDs( $ids ) { 00395 if ( !count( $ids ) ) { 00396 return array(); 00397 } 00398 $dbr = wfGetDB( DB_SLAVE ); 00399 00400 $res = $dbr->select( 00401 'page', 00402 self::getSelectFields(), 00403 array( 'page_id' => $ids ), 00404 __METHOD__ 00405 ); 00406 00407 $titles = array(); 00408 foreach ( $res as $row ) { 00409 $titles[] = Title::newFromRow( $row ); 00410 } 00411 return $titles; 00412 } 00413 00420 public static function newFromRow( $row ) { 00421 $t = self::makeTitle( $row->page_namespace, $row->page_title ); 00422 $t->loadFromRow( $row ); 00423 return $t; 00424 } 00425 00432 public function loadFromRow( $row ) { 00433 if ( $row ) { // page found 00434 if ( isset( $row->page_id ) ) { 00435 $this->mArticleID = (int)$row->page_id; 00436 } 00437 if ( isset( $row->page_len ) ) { 00438 $this->mLength = (int)$row->page_len; 00439 } 00440 if ( isset( $row->page_is_redirect ) ) { 00441 $this->mRedirect = (bool)$row->page_is_redirect; 00442 } 00443 if ( isset( $row->page_latest ) ) { 00444 $this->mLatestID = (int)$row->page_latest; 00445 } 00446 if ( isset( $row->page_content_model ) ) { 00447 $this->mContentModel = strval( $row->page_content_model ); 00448 } else { 00449 $this->mContentModel = false; # initialized lazily in getContentModel() 00450 } 00451 if ( isset( $row->page_lang ) ) { 00452 $this->mDbPageLanguage = (string)$row->page_lang; 00453 } 00454 } else { // page not found 00455 $this->mArticleID = 0; 00456 $this->mLength = 0; 00457 $this->mRedirect = false; 00458 $this->mLatestID = 0; 00459 $this->mContentModel = false; # initialized lazily in getContentModel() 00460 } 00461 } 00462 00476 public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { 00477 $t = new Title(); 00478 $t->mInterwiki = $interwiki; 00479 $t->mFragment = $fragment; 00480 $t->mNamespace = $ns = intval( $ns ); 00481 $t->mDbkeyform = str_replace( ' ', '_', $title ); 00482 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; 00483 $t->mUrlform = wfUrlencode( $t->mDbkeyform ); 00484 $t->mTextform = str_replace( '_', ' ', $title ); 00485 $t->mContentModel = false; # initialized lazily in getContentModel() 00486 return $t; 00487 } 00488 00500 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { 00501 if ( !MWNamespace::exists( $ns ) ) { 00502 return null; 00503 } 00504 00505 $t = new Title(); 00506 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki ); 00507 if ( $t->secureAndSplit() ) { 00508 return $t; 00509 } else { 00510 return null; 00511 } 00512 } 00513 00519 public static function newMainPage() { 00520 $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() ); 00521 // Don't give fatal errors if the message is broken 00522 if ( !$title ) { 00523 $title = Title::newFromText( 'Main Page' ); 00524 } 00525 return $title; 00526 } 00527 00538 public static function newFromRedirect( $text ) { 00539 ContentHandler::deprecated( __METHOD__, '1.21' ); 00540 00541 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00542 return $content->getRedirectTarget(); 00543 } 00544 00555 public static function newFromRedirectRecurse( $text ) { 00556 ContentHandler::deprecated( __METHOD__, '1.21' ); 00557 00558 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00559 return $content->getUltimateRedirectTarget(); 00560 } 00561 00572 public static function newFromRedirectArray( $text ) { 00573 ContentHandler::deprecated( __METHOD__, '1.21' ); 00574 00575 $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT ); 00576 return $content->getRedirectChain(); 00577 } 00578 00585 public static function nameOf( $id ) { 00586 $dbr = wfGetDB( DB_SLAVE ); 00587 00588 $s = $dbr->selectRow( 00589 'page', 00590 array( 'page_namespace', 'page_title' ), 00591 array( 'page_id' => $id ), 00592 __METHOD__ 00593 ); 00594 if ( $s === false ) { 00595 return null; 00596 } 00597 00598 $n = self::makeName( $s->page_namespace, $s->page_title ); 00599 return $n; 00600 } 00601 00607 public static function legalChars() { 00608 global $wgLegalTitleChars; 00609 return $wgLegalTitleChars; 00610 } 00611 00621 static function getTitleInvalidRegex() { 00622 static $rxTc = false; 00623 if ( !$rxTc ) { 00624 # Matching titles will be held as illegal. 00625 $rxTc = '/' . 00626 # Any character not allowed is forbidden... 00627 '[^' . self::legalChars() . ']' . 00628 # URL percent encoding sequences interfere with the ability 00629 # to round-trip titles -- you can't link to them consistently. 00630 '|%[0-9A-Fa-f]{2}' . 00631 # XML/HTML character references produce similar issues. 00632 '|&[A-Za-z0-9\x80-\xff]+;' . 00633 '|&#[0-9]+;' . 00634 '|&#x[0-9A-Fa-f]+;' . 00635 '/S'; 00636 } 00637 00638 return $rxTc; 00639 } 00640 00650 public static function convertByteClassToUnicodeClass( $byteClass ) { 00651 $length = strlen( $byteClass ); 00652 // Input token queue 00653 $x0 = $x1 = $x2 = ''; 00654 // Decoded queue 00655 $d0 = $d1 = $d2 = ''; 00656 // Decoded integer codepoints 00657 $ord0 = $ord1 = $ord2 = 0; 00658 // Re-encoded queue 00659 $r0 = $r1 = $r2 = ''; 00660 // Output 00661 $out = ''; 00662 // Flags 00663 $allowUnicode = false; 00664 for ( $pos = 0; $pos < $length; $pos++ ) { 00665 // Shift the queues down 00666 $x2 = $x1; 00667 $x1 = $x0; 00668 $d2 = $d1; 00669 $d1 = $d0; 00670 $ord2 = $ord1; 00671 $ord1 = $ord0; 00672 $r2 = $r1; 00673 $r1 = $r0; 00674 // Load the current input token and decoded values 00675 $inChar = $byteClass[$pos]; 00676 if ( $inChar == '\\' ) { 00677 if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) { 00678 $x0 = $inChar . $m[0]; 00679 $d0 = chr( hexdec( $m[1] ) ); 00680 $pos += strlen( $m[0] ); 00681 } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) { 00682 $x0 = $inChar . $m[0]; 00683 $d0 = chr( octdec( $m[0] ) ); 00684 $pos += strlen( $m[0] ); 00685 } elseif ( $pos + 1 >= $length ) { 00686 $x0 = $d0 = '\\'; 00687 } else { 00688 $d0 = $byteClass[$pos + 1]; 00689 $x0 = $inChar . $d0; 00690 $pos += 1; 00691 } 00692 } else { 00693 $x0 = $d0 = $inChar; 00694 } 00695 $ord0 = ord( $d0 ); 00696 // Load the current re-encoded value 00697 if ( $ord0 < 32 || $ord0 == 0x7f ) { 00698 $r0 = sprintf( '\x%02x', $ord0 ); 00699 } elseif ( $ord0 >= 0x80 ) { 00700 // Allow unicode if a single high-bit character appears 00701 $r0 = sprintf( '\x%02x', $ord0 ); 00702 $allowUnicode = true; 00703 } elseif ( strpos( '-\\[]^', $d0 ) !== false ) { 00704 $r0 = '\\' . $d0; 00705 } else { 00706 $r0 = $d0; 00707 } 00708 // Do the output 00709 if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) { 00710 // Range 00711 if ( $ord2 > $ord0 ) { 00712 // Empty range 00713 } elseif ( $ord0 >= 0x80 ) { 00714 // Unicode range 00715 $allowUnicode = true; 00716 if ( $ord2 < 0x80 ) { 00717 // Keep the non-unicode section of the range 00718 $out .= "$r2-\\x7F"; 00719 } 00720 } else { 00721 // Normal range 00722 $out .= "$r2-$r0"; 00723 } 00724 // Reset state to the initial value 00725 $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = ''; 00726 } elseif ( $ord2 < 0x80 ) { 00727 // ASCII character 00728 $out .= $r2; 00729 } 00730 } 00731 if ( $ord1 < 0x80 ) { 00732 $out .= $r1; 00733 } 00734 if ( $ord0 < 0x80 ) { 00735 $out .= $r0; 00736 } 00737 if ( $allowUnicode ) { 00738 $out .= '\u0080-\uFFFF'; 00739 } 00740 return $out; 00741 } 00742 00752 public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) { 00753 global $wgContLang; 00754 00755 $namespace = $wgContLang->getNsText( $ns ); 00756 $name = $namespace == '' ? $title : "$namespace:$title"; 00757 if ( strval( $interwiki ) != '' ) { 00758 $name = "$interwiki:$name"; 00759 } 00760 if ( strval( $fragment ) != '' ) { 00761 $name .= '#' . $fragment; 00762 } 00763 return $name; 00764 } 00765 00772 static function escapeFragmentForURL( $fragment ) { 00773 # Note that we don't urlencode the fragment. urlencoded Unicode 00774 # fragments appear not to work in IE (at least up to 7) or in at least 00775 # one version of Opera 9.x. The W3C validator, for one, doesn't seem 00776 # to care if they aren't encoded. 00777 return Sanitizer::escapeId( $fragment, 'noninitial' ); 00778 } 00779 00788 public static function compare( $a, $b ) { 00789 if ( $a->getNamespace() == $b->getNamespace() ) { 00790 return strcmp( $a->getText(), $b->getText() ); 00791 } else { 00792 return $a->getNamespace() - $b->getNamespace(); 00793 } 00794 } 00795 00802 public function isLocal() { 00803 if ( $this->isExternal() ) { 00804 $iw = Interwiki::fetch( $this->mInterwiki ); 00805 if ( $iw ) { 00806 return $iw->isLocal(); 00807 } 00808 } 00809 return true; 00810 } 00811 00817 public function isExternal() { 00818 return $this->mInterwiki !== ''; 00819 } 00820 00828 public function getInterwiki() { 00829 return $this->mInterwiki; 00830 } 00831 00837 public function wasLocalInterwiki() { 00838 return $this->mLocalInterwiki; 00839 } 00840 00847 public function isTrans() { 00848 if ( !$this->isExternal() ) { 00849 return false; 00850 } 00851 00852 return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); 00853 } 00854 00860 public function getTransWikiID() { 00861 if ( !$this->isExternal() ) { 00862 return false; 00863 } 00864 00865 return Interwiki::fetch( $this->mInterwiki )->getWikiID(); 00866 } 00867 00877 public function getTitleValue() { 00878 if ( $this->mTitleValue === null ) { 00879 try { 00880 $this->mTitleValue = new TitleValue( 00881 $this->getNamespace(), 00882 $this->getDBkey(), 00883 $this->getFragment() ); 00884 } catch ( InvalidArgumentException $ex ) { 00885 wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' . 00886 $this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" ); 00887 } 00888 } 00889 00890 return $this->mTitleValue; 00891 } 00892 00898 public function getText() { 00899 return $this->mTextform; 00900 } 00901 00907 public function getPartialURL() { 00908 return $this->mUrlform; 00909 } 00910 00916 public function getDBkey() { 00917 return $this->mDbkeyform; 00918 } 00919 00925 function getUserCaseDBKey() { 00926 if ( !is_null( $this->mUserCaseDBKey ) ) { 00927 return $this->mUserCaseDBKey; 00928 } else { 00929 // If created via makeTitle(), $this->mUserCaseDBKey is not set. 00930 return $this->mDbkeyform; 00931 } 00932 } 00933 00939 public function getNamespace() { 00940 return $this->mNamespace; 00941 } 00942 00950 public function getContentModel( $flags = 0 ) { 00951 # Calling getArticleID() loads the field from cache as needed 00952 if ( !$this->mContentModel && $this->getArticleID( $flags ) ) { 00953 $linkCache = LinkCache::singleton(); 00954 $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); 00955 } 00956 00957 if ( !$this->mContentModel ) { 00958 $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); 00959 } 00960 00961 if ( !$this->mContentModel ) { 00962 throw new MWException( 'Failed to determine content model!' ); 00963 } 00964 00965 return $this->mContentModel; 00966 } 00967 00974 public function hasContentModel( $id ) { 00975 return $this->getContentModel() == $id; 00976 } 00977 00983 public function getNsText() { 00984 if ( $this->isExternal() ) { 00985 // This probably shouldn't even happen. ohh man, oh yuck. 00986 // But for interwiki transclusion it sometimes does. 00987 // Shit. Shit shit shit. 00988 // 00989 // Use the canonical namespaces if possible to try to 00990 // resolve a foreign namespace. 00991 if ( MWNamespace::exists( $this->mNamespace ) ) { 00992 return MWNamespace::getCanonicalName( $this->mNamespace ); 00993 } 00994 } 00995 00996 try { 00997 $formatter = self::getTitleFormatter(); 00998 return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform ); 00999 } catch ( InvalidArgumentException $ex ) { 01000 wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" ); 01001 return false; 01002 } 01003 } 01004 01010 public function getSubjectNsText() { 01011 global $wgContLang; 01012 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) ); 01013 } 01014 01020 public function getTalkNsText() { 01021 global $wgContLang; 01022 return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ); 01023 } 01024 01030 public function canTalk() { 01031 return MWNamespace::canTalk( $this->mNamespace ); 01032 } 01033 01040 public function canExist() { 01041 return $this->mNamespace >= NS_MAIN; 01042 } 01043 01049 public function isWatchable() { 01050 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); 01051 } 01052 01058 public function isSpecialPage() { 01059 return $this->getNamespace() == NS_SPECIAL; 01060 } 01061 01068 public function isSpecial( $name ) { 01069 if ( $this->isSpecialPage() ) { 01070 list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); 01071 if ( $name == $thisName ) { 01072 return true; 01073 } 01074 } 01075 return false; 01076 } 01077 01084 public function fixSpecialName() { 01085 if ( $this->isSpecialPage() ) { 01086 list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); 01087 if ( $canonicalName ) { 01088 $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); 01089 if ( $localName != $this->mDbkeyform ) { 01090 return Title::makeTitle( NS_SPECIAL, $localName ); 01091 } 01092 } 01093 } 01094 return $this; 01095 } 01096 01107 public function inNamespace( $ns ) { 01108 return MWNamespace::equals( $this->getNamespace(), $ns ); 01109 } 01110 01118 public function inNamespaces( /* ... */ ) { 01119 $namespaces = func_get_args(); 01120 if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { 01121 $namespaces = $namespaces[0]; 01122 } 01123 01124 foreach ( $namespaces as $ns ) { 01125 if ( $this->inNamespace( $ns ) ) { 01126 return true; 01127 } 01128 } 01129 01130 return false; 01131 } 01132 01146 public function hasSubjectNamespace( $ns ) { 01147 return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); 01148 } 01149 01157 public function isContentPage() { 01158 return MWNamespace::isContent( $this->getNamespace() ); 01159 } 01160 01167 public function isMovable() { 01168 if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) { 01169 // Interwiki title or immovable namespace. Hooks don't get to override here 01170 return false; 01171 } 01172 01173 $result = true; 01174 wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); 01175 return $result; 01176 } 01177 01188 public function isMainPage() { 01189 return $this->equals( Title::newMainPage() ); 01190 } 01191 01197 public function isSubpage() { 01198 return MWNamespace::hasSubpages( $this->mNamespace ) 01199 ? strpos( $this->getText(), '/' ) !== false 01200 : false; 01201 } 01202 01208 public function isConversionTable() { 01209 // @todo ConversionTable should become a separate content model. 01210 01211 return $this->getNamespace() == NS_MEDIAWIKI && 01212 strpos( $this->getText(), 'Conversiontable/' ) === 0; 01213 } 01214 01220 public function isWikitextPage() { 01221 return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); 01222 } 01223 01237 public function isCssOrJsPage() { 01238 $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace 01239 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 01240 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01241 01242 # @note This hook is also called in ContentHandler::getDefaultModel. 01243 # It's called here again to make sure hook functions can force this 01244 # method to return true even outside the mediawiki namespace. 01245 01246 wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) ); 01247 01248 return $isCssOrJsPage; 01249 } 01250 01255 public function isCssJsSubpage() { 01256 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01257 && ( $this->hasContentModel( CONTENT_MODEL_CSS ) 01258 || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) ); 01259 } 01260 01266 public function getSkinFromCssJsSubpage() { 01267 $subpage = explode( '/', $this->mTextform ); 01268 $subpage = $subpage[count( $subpage ) - 1]; 01269 $lastdot = strrpos( $subpage, '.' ); 01270 if ( $lastdot === false ) { 01271 return $subpage; # Never happens: only called for names ending in '.css' or '.js' 01272 } 01273 return substr( $subpage, 0, $lastdot ); 01274 } 01275 01281 public function isCssSubpage() { 01282 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01283 && $this->hasContentModel( CONTENT_MODEL_CSS ) ); 01284 } 01285 01291 public function isJsSubpage() { 01292 return ( NS_USER == $this->mNamespace && $this->isSubpage() 01293 && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ); 01294 } 01295 01301 public function isTalkPage() { 01302 return MWNamespace::isTalk( $this->getNamespace() ); 01303 } 01304 01310 public function getTalkPage() { 01311 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); 01312 } 01313 01320 public function getSubjectPage() { 01321 // Is this the same title? 01322 $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); 01323 if ( $this->getNamespace() == $subjectNS ) { 01324 return $this; 01325 } 01326 return Title::makeTitle( $subjectNS, $this->getDBkey() ); 01327 } 01328 01334 public function getDefaultNamespace() { 01335 return $this->mDefaultNamespace; 01336 } 01337 01345 public function getFragment() { 01346 return $this->mFragment; 01347 } 01348 01355 public function hasFragment() { 01356 return $this->mFragment !== ''; 01357 } 01358 01363 public function getFragmentForURL() { 01364 if ( !$this->hasFragment() ) { 01365 return ''; 01366 } else { 01367 return '#' . Title::escapeFragmentForURL( $this->getFragment() ); 01368 } 01369 } 01370 01381 public function setFragment( $fragment ) { 01382 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); 01383 } 01384 01392 private function prefix( $name ) { 01393 $p = ''; 01394 if ( $this->isExternal() ) { 01395 $p = $this->mInterwiki . ':'; 01396 } 01397 01398 if ( 0 != $this->mNamespace ) { 01399 $p .= $this->getNsText() . ':'; 01400 } 01401 return $p . $name; 01402 } 01403 01410 public function getPrefixedDBkey() { 01411 $s = $this->prefix( $this->mDbkeyform ); 01412 $s = str_replace( ' ', '_', $s ); 01413 return $s; 01414 } 01415 01422 public function getPrefixedText() { 01423 if ( $this->mPrefixedText === null ) { 01424 $s = $this->prefix( $this->mTextform ); 01425 $s = str_replace( '_', ' ', $s ); 01426 $this->mPrefixedText = $s; 01427 } 01428 return $this->mPrefixedText; 01429 } 01430 01436 public function __toString() { 01437 return $this->getPrefixedText(); 01438 } 01439 01446 public function getFullText() { 01447 $text = $this->getPrefixedText(); 01448 if ( $this->hasFragment() ) { 01449 $text .= '#' . $this->getFragment(); 01450 } 01451 return $text; 01452 } 01453 01466 public function getRootText() { 01467 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01468 return $this->getText(); 01469 } 01470 01471 return strtok( $this->getText(), '/' ); 01472 } 01473 01486 public function getRootTitle() { 01487 return Title::makeTitle( $this->getNamespace(), $this->getRootText() ); 01488 } 01489 01501 public function getBaseText() { 01502 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01503 return $this->getText(); 01504 } 01505 01506 $parts = explode( '/', $this->getText() ); 01507 # Don't discard the real title if there's no subpage involved 01508 if ( count( $parts ) > 1 ) { 01509 unset( $parts[count( $parts ) - 1] ); 01510 } 01511 return implode( '/', $parts ); 01512 } 01513 01526 public function getBaseTitle() { 01527 return Title::makeTitle( $this->getNamespace(), $this->getBaseText() ); 01528 } 01529 01541 public function getSubpageText() { 01542 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01543 return $this->mTextform; 01544 } 01545 $parts = explode( '/', $this->mTextform ); 01546 return $parts[count( $parts ) - 1]; 01547 } 01548 01562 public function getSubpage( $text ) { 01563 return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text ); 01564 } 01565 01571 public function getSubpageUrlForm() { 01572 $text = $this->getSubpageText(); 01573 $text = wfUrlencode( str_replace( ' ', '_', $text ) ); 01574 return $text; 01575 } 01576 01582 public function getPrefixedURL() { 01583 $s = $this->prefix( $this->mDbkeyform ); 01584 $s = wfUrlencode( str_replace( ' ', '_', $s ) ); 01585 return $s; 01586 } 01587 01601 private static function fixUrlQueryArgs( $query, $query2 = false ) { 01602 if ( $query2 !== false ) { 01603 wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " . 01604 "method called with a second parameter is deprecated. Add your " . 01605 "parameter to an array passed as the first parameter.", "1.19" ); 01606 } 01607 if ( is_array( $query ) ) { 01608 $query = wfArrayToCgi( $query ); 01609 } 01610 if ( $query2 ) { 01611 if ( is_string( $query2 ) ) { 01612 // $query2 is a string, we will consider this to be 01613 // a deprecated $variant argument and add it to the query 01614 $query2 = wfArrayToCgi( array( 'variant' => $query2 ) ); 01615 } else { 01616 $query2 = wfArrayToCgi( $query2 ); 01617 } 01618 // If we have $query content add a & to it first 01619 if ( $query ) { 01620 $query .= '&'; 01621 } 01622 // Now append the queries together 01623 $query .= $query2; 01624 } 01625 return $query; 01626 } 01627 01639 public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01640 $query = self::fixUrlQueryArgs( $query, $query2 ); 01641 01642 # Hand off all the decisions on urls to getLocalURL 01643 $url = $this->getLocalURL( $query ); 01644 01645 # Expand the url to make it a full url. Note that getLocalURL has the 01646 # potential to output full urls for a variety of reasons, so we use 01647 # wfExpandUrl instead of simply prepending $wgServer 01648 $url = wfExpandUrl( $url, $proto ); 01649 01650 # Finally, add the fragment. 01651 $url .= $this->getFragmentForURL(); 01652 01653 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); 01654 return $url; 01655 } 01656 01680 public function getLocalURL( $query = '', $query2 = false ) { 01681 global $wgArticlePath, $wgScript, $wgServer, $wgRequest; 01682 01683 $query = self::fixUrlQueryArgs( $query, $query2 ); 01684 01685 $interwiki = Interwiki::fetch( $this->mInterwiki ); 01686 if ( $interwiki ) { 01687 $namespace = $this->getNsText(); 01688 if ( $namespace != '' ) { 01689 # Can this actually happen? Interwikis shouldn't be parsed. 01690 # Yes! It can in interwiki transclusion. But... it probably shouldn't. 01691 $namespace .= ':'; 01692 } 01693 $url = $interwiki->getURL( $namespace . $this->getDBkey() ); 01694 $url = wfAppendQuery( $url, $query ); 01695 } else { 01696 $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); 01697 if ( $query == '' ) { 01698 $url = str_replace( '$1', $dbkey, $wgArticlePath ); 01699 wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); 01700 } else { 01701 global $wgVariantArticlePath, $wgActionPaths, $wgContLang; 01702 $url = false; 01703 $matches = array(); 01704 01705 if ( !empty( $wgActionPaths ) 01706 && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) 01707 ) { 01708 $action = urldecode( $matches[2] ); 01709 if ( isset( $wgActionPaths[$action] ) ) { 01710 $query = $matches[1]; 01711 if ( isset( $matches[4] ) ) { 01712 $query .= $matches[4]; 01713 } 01714 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); 01715 if ( $query != '' ) { 01716 $url = wfAppendQuery( $url, $query ); 01717 } 01718 } 01719 } 01720 01721 if ( $url === false 01722 && $wgVariantArticlePath 01723 && $wgContLang->getCode() === $this->getPageLanguage()->getCode() 01724 && $this->getPageLanguage()->hasVariants() 01725 && preg_match( '/^variant=([^&]*)$/', $query, $matches ) 01726 ) { 01727 $variant = urldecode( $matches[1] ); 01728 if ( $this->getPageLanguage()->hasVariant( $variant ) ) { 01729 // Only do the variant replacement if the given variant is a valid 01730 // variant for the page's language. 01731 $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); 01732 $url = str_replace( '$1', $dbkey, $url ); 01733 } 01734 } 01735 01736 if ( $url === false ) { 01737 if ( $query == '-' ) { 01738 $query = ''; 01739 } 01740 $url = "{$wgScript}?title={$dbkey}&{$query}"; 01741 } 01742 } 01743 01744 wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) ); 01745 01746 // @todo FIXME: This causes breakage in various places when we 01747 // actually expected a local URL and end up with dupe prefixes. 01748 if ( $wgRequest->getVal( 'action' ) == 'render' ) { 01749 $url = $wgServer . $url; 01750 } 01751 } 01752 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) ); 01753 return $url; 01754 } 01755 01772 public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { 01773 wfProfileIn( __METHOD__ ); 01774 if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) { 01775 $ret = $this->getFullURL( $query, $query2, $proto ); 01776 } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) { 01777 $ret = $this->getFragmentForURL(); 01778 } else { 01779 $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL(); 01780 } 01781 wfProfileOut( __METHOD__ ); 01782 return $ret; 01783 } 01784 01797 public function getInternalURL( $query = '', $query2 = false ) { 01798 global $wgInternalServer, $wgServer; 01799 $query = self::fixUrlQueryArgs( $query, $query2 ); 01800 $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; 01801 $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP ); 01802 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); 01803 return $url; 01804 } 01805 01817 public function getCanonicalURL( $query = '', $query2 = false ) { 01818 $query = self::fixUrlQueryArgs( $query, $query2 ); 01819 $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL ); 01820 wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) ); 01821 return $url; 01822 } 01823 01829 public function getEditURL() { 01830 if ( $this->isExternal() ) { 01831 return ''; 01832 } 01833 $s = $this->getLocalURL( 'action=edit' ); 01834 01835 return $s; 01836 } 01837 01844 public function userIsWatching() { 01845 global $wgUser; 01846 01847 if ( is_null( $this->mWatched ) ) { 01848 if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) { 01849 $this->mWatched = false; 01850 } else { 01851 $this->mWatched = $wgUser->isWatched( $this ); 01852 } 01853 } 01854 return $this->mWatched; 01855 } 01856 01871 public function quickUserCan( $action, $user = null ) { 01872 return $this->userCan( $action, $user, false ); 01873 } 01874 01885 public function userCan( $action, $user = null, $doExpensiveQueries = true ) { 01886 if ( !$user instanceof User ) { 01887 global $wgUser; 01888 $user = $wgUser; 01889 } 01890 01891 return !count( $this->getUserPermissionsErrorsInternal( 01892 $action, $user, $doExpensiveQueries, true ) ); 01893 } 01894 01908 public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, 01909 $ignoreErrors = array() 01910 ) { 01911 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); 01912 01913 // Remove the errors being ignored. 01914 foreach ( $errors as $index => $error ) { 01915 $error_key = is_array( $error ) ? $error[0] : $error; 01916 01917 if ( in_array( $error_key, $ignoreErrors ) ) { 01918 unset( $errors[$index] ); 01919 } 01920 } 01921 01922 return $errors; 01923 } 01924 01936 private function checkQuickPermissions( $action, $user, $errors, 01937 $doExpensiveQueries, $short 01938 ) { 01939 if ( !wfRunHooks( 'TitleQuickPermissions', 01940 array( $this, $user, $action, &$errors, $doExpensiveQueries, $short ) ) 01941 ) { 01942 return $errors; 01943 } 01944 01945 if ( $action == 'create' ) { 01946 if ( 01947 ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || 01948 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) 01949 ) { 01950 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); 01951 } 01952 } elseif ( $action == 'move' ) { 01953 if ( !$user->isAllowed( 'move-rootuserpages' ) 01954 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01955 // Show user page-specific message only if the user can move other pages 01956 $errors[] = array( 'cant-move-user-page' ); 01957 } 01958 01959 // Check if user is allowed to move files if it's a file 01960 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { 01961 $errors[] = array( 'movenotallowedfile' ); 01962 } 01963 01964 // Check if user is allowed to move category pages if it's a category page 01965 if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) { 01966 $errors[] = array( 'cant-move-category-page' ); 01967 } 01968 01969 if ( !$user->isAllowed( 'move' ) ) { 01970 // User can't move anything 01971 $userCanMove = User::groupHasPermission( 'user', 'move' ); 01972 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' ); 01973 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { 01974 // custom message if logged-in users without any special rights can move 01975 $errors[] = array( 'movenologintext' ); 01976 } else { 01977 $errors[] = array( 'movenotallowed' ); 01978 } 01979 } 01980 } elseif ( $action == 'move-target' ) { 01981 if ( !$user->isAllowed( 'move' ) ) { 01982 // User can't move anything 01983 $errors[] = array( 'movenotallowed' ); 01984 } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 01985 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01986 // Show user page-specific message only if the user can move other pages 01987 $errors[] = array( 'cant-move-to-user-page' ); 01988 } elseif ( !$user->isAllowed( 'move-categorypages' ) 01989 && $this->mNamespace == NS_CATEGORY ) { 01990 // Show category page-specific message only if the user can move other pages 01991 $errors[] = array( 'cant-move-to-category-page' ); 01992 } 01993 } elseif ( !$user->isAllowed( $action ) ) { 01994 $errors[] = $this->missingPermissionError( $action, $short ); 01995 } 01996 01997 return $errors; 01998 } 01999 02008 private function resultToError( $errors, $result ) { 02009 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { 02010 // A single array representing an error 02011 $errors[] = $result; 02012 } elseif ( is_array( $result ) && is_array( $result[0] ) ) { 02013 // A nested array representing multiple errors 02014 $errors = array_merge( $errors, $result ); 02015 } elseif ( $result !== '' && is_string( $result ) ) { 02016 // A string representing a message-id 02017 $errors[] = array( $result ); 02018 } elseif ( $result === false ) { 02019 // a generic "We don't want them to do that" 02020 $errors[] = array( 'badaccess-group0' ); 02021 } 02022 return $errors; 02023 } 02024 02036 private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { 02037 // Use getUserPermissionsErrors instead 02038 $result = ''; 02039 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { 02040 return $result ? array() : array( array( 'badaccess-group0' ) ); 02041 } 02042 // Check getUserPermissionsErrors hook 02043 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { 02044 $errors = $this->resultToError( $errors, $result ); 02045 } 02046 // Check getUserPermissionsErrorsExpensive hook 02047 if ( 02048 $doExpensiveQueries 02049 && !( $short && count( $errors ) > 0 ) 02050 && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) 02051 ) { 02052 $errors = $this->resultToError( $errors, $result ); 02053 } 02054 02055 return $errors; 02056 } 02057 02069 private function checkSpecialsAndNSPermissions( $action, $user, $errors, 02070 $doExpensiveQueries, $short 02071 ) { 02072 # Only 'createaccount' can be performed on special pages, 02073 # which don't actually exist in the DB. 02074 if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) { 02075 $errors[] = array( 'ns-specialprotected' ); 02076 } 02077 02078 # Check $wgNamespaceProtection for restricted namespaces 02079 if ( $this->isNamespaceProtected( $user ) ) { 02080 $ns = $this->mNamespace == NS_MAIN ? 02081 wfMessage( 'nstab-main' )->text() : $this->getNsText(); 02082 $errors[] = $this->mNamespace == NS_MEDIAWIKI ? 02083 array( 'protectedinterface', $action ) : array( 'namespaceprotected', $ns, $action ); 02084 } 02085 02086 return $errors; 02087 } 02088 02100 private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02101 # Protect css/js subpages of user pages 02102 # XXX: this might be better using restrictions 02103 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only 02104 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) { 02105 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { 02106 if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) { 02107 $errors[] = array( 'mycustomcssprotected', $action ); 02108 } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) { 02109 $errors[] = array( 'mycustomjsprotected', $action ); 02110 } 02111 } else { 02112 if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { 02113 $errors[] = array( 'customcssprotected', $action ); 02114 } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { 02115 $errors[] = array( 'customjsprotected', $action ); 02116 } 02117 } 02118 } 02119 02120 return $errors; 02121 } 02122 02136 private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02137 foreach ( $this->getRestrictions( $action ) as $right ) { 02138 // Backwards compatibility, rewrite sysop -> editprotected 02139 if ( $right == 'sysop' ) { 02140 $right = 'editprotected'; 02141 } 02142 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected 02143 if ( $right == 'autoconfirmed' ) { 02144 $right = 'editsemiprotected'; 02145 } 02146 if ( $right == '' ) { 02147 continue; 02148 } 02149 if ( !$user->isAllowed( $right ) ) { 02150 $errors[] = array( 'protectedpagetext', $right, $action ); 02151 } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) { 02152 $errors[] = array( 'protectedpagetext', 'protect', $action ); 02153 } 02154 } 02155 02156 return $errors; 02157 } 02158 02170 private function checkCascadingSourcesRestrictions( $action, $user, $errors, 02171 $doExpensiveQueries, $short 02172 ) { 02173 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { 02174 # We /could/ use the protection level on the source page, but it's 02175 # fairly ugly as we have to establish a precedence hierarchy for pages 02176 # included by multiple cascade-protected pages. So just restrict 02177 # it to people with 'protect' permission, as they could remove the 02178 # protection anyway. 02179 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); 02180 # Cascading protection depends on more than this page... 02181 # Several cascading protected pages may include this page... 02182 # Check each cascading level 02183 # This is only for protection restrictions, not for all actions 02184 if ( isset( $restrictions[$action] ) ) { 02185 foreach ( $restrictions[$action] as $right ) { 02186 // Backwards compatibility, rewrite sysop -> editprotected 02187 if ( $right == 'sysop' ) { 02188 $right = 'editprotected'; 02189 } 02190 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected 02191 if ( $right == 'autoconfirmed' ) { 02192 $right = 'editsemiprotected'; 02193 } 02194 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) { 02195 $pages = ''; 02196 foreach ( $cascadingSources as $page ) { 02197 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; 02198 } 02199 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages, $action ); 02200 } 02201 } 02202 } 02203 } 02204 02205 return $errors; 02206 } 02207 02219 private function checkActionPermissions( $action, $user, $errors, 02220 $doExpensiveQueries, $short 02221 ) { 02222 global $wgDeleteRevisionsLimit, $wgLang; 02223 02224 if ( $action == 'protect' ) { 02225 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', 02226 $user, $doExpensiveQueries, true ) ) 02227 ) { 02228 // If they can't edit, they shouldn't protect. 02229 $errors[] = array( 'protect-cantedit' ); 02230 } 02231 } elseif ( $action == 'create' ) { 02232 $title_protection = $this->getTitleProtection(); 02233 if ( $title_protection ) { 02234 if ( $title_protection['pt_create_perm'] == 'sysop' ) { 02235 $title_protection['pt_create_perm'] = 'editprotected'; // B/C 02236 } 02237 if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) { 02238 $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C 02239 } 02240 if ( $title_protection['pt_create_perm'] == '' 02241 || !$user->isAllowed( $title_protection['pt_create_perm'] ) 02242 ) { 02243 $errors[] = array( 02244 'titleprotected', 02245 User::whoIs( $title_protection['pt_user'] ), 02246 $title_protection['pt_reason'] 02247 ); 02248 } 02249 } 02250 } elseif ( $action == 'move' ) { 02251 // Check for immobile pages 02252 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02253 // Specific message for this case 02254 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 02255 } elseif ( !$this->isMovable() ) { 02256 // Less specific message for rarer cases 02257 $errors[] = array( 'immobile-source-page' ); 02258 } 02259 } elseif ( $action == 'move-target' ) { 02260 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 02261 $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); 02262 } elseif ( !$this->isMovable() ) { 02263 $errors[] = array( 'immobile-target-page' ); 02264 } 02265 } elseif ( $action == 'delete' ) { 02266 $tempErrors = $this->checkPageRestrictions( 'edit', 02267 $user, array(), $doExpensiveQueries, true ); 02268 if ( !$tempErrors ) { 02269 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit', 02270 $user, $tempErrors, $doExpensiveQueries, true ); 02271 } 02272 if ( $tempErrors ) { 02273 // If protection keeps them from editing, they shouldn't be able to delete. 02274 $errors[] = array( 'deleteprotected' ); 02275 } 02276 if ( $doExpensiveQueries && $wgDeleteRevisionsLimit 02277 && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() 02278 ) { 02279 $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); 02280 } 02281 } 02282 return $errors; 02283 } 02284 02296 private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { 02297 // Account creation blocks handled at userlogin. 02298 // Unblocking handled in SpecialUnblock 02299 if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { 02300 return $errors; 02301 } 02302 02303 global $wgEmailConfirmToEdit; 02304 02305 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { 02306 $errors[] = array( 'confirmedittext' ); 02307 } 02308 02309 if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) { 02310 // Don't block the user from editing their own talk page unless they've been 02311 // explicitly blocked from that too. 02312 } elseif ( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { 02313 // @todo FIXME: Pass the relevant context into this function. 02314 $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() ); 02315 } 02316 02317 return $errors; 02318 } 02319 02331 private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 02332 global $wgWhitelistRead, $wgWhitelistReadRegexp; 02333 02334 $whitelisted = false; 02335 if ( User::isEveryoneAllowed( 'read' ) ) { 02336 # Shortcut for public wikis, allows skipping quite a bit of code 02337 $whitelisted = true; 02338 } elseif ( $user->isAllowed( 'read' ) ) { 02339 # If the user is allowed to read pages, he is allowed to read all pages 02340 $whitelisted = true; 02341 } elseif ( $this->isSpecial( 'Userlogin' ) 02342 || $this->isSpecial( 'ChangePassword' ) 02343 || $this->isSpecial( 'PasswordReset' ) 02344 ) { 02345 # Always grant access to the login page. 02346 # Even anons need to be able to log in. 02347 $whitelisted = true; 02348 } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { 02349 # Time to check the whitelist 02350 # Only do these checks is there's something to check against 02351 $name = $this->getPrefixedText(); 02352 $dbName = $this->getPrefixedDBkey(); 02353 02354 // Check for explicit whitelisting with and without underscores 02355 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { 02356 $whitelisted = true; 02357 } elseif ( $this->getNamespace() == NS_MAIN ) { 02358 # Old settings might have the title prefixed with 02359 # a colon for main-namespace pages 02360 if ( in_array( ':' . $name, $wgWhitelistRead ) ) { 02361 $whitelisted = true; 02362 } 02363 } elseif ( $this->isSpecialPage() ) { 02364 # If it's a special page, ditch the subpage bit and check again 02365 $name = $this->getDBkey(); 02366 list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); 02367 if ( $name ) { 02368 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); 02369 if ( in_array( $pure, $wgWhitelistRead, true ) ) { 02370 $whitelisted = true; 02371 } 02372 } 02373 } 02374 } 02375 02376 if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { 02377 $name = $this->getPrefixedText(); 02378 // Check for regex whitelisting 02379 foreach ( $wgWhitelistReadRegexp as $listItem ) { 02380 if ( preg_match( $listItem, $name ) ) { 02381 $whitelisted = true; 02382 break; 02383 } 02384 } 02385 } 02386 02387 if ( !$whitelisted ) { 02388 # If the title is not whitelisted, give extensions a chance to do so... 02389 wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); 02390 if ( !$whitelisted ) { 02391 $errors[] = $this->missingPermissionError( $action, $short ); 02392 } 02393 } 02394 02395 return $errors; 02396 } 02397 02406 private function missingPermissionError( $action, $short ) { 02407 // We avoid expensive display logic for quickUserCan's and such 02408 if ( $short ) { 02409 return array( 'badaccess-group0' ); 02410 } 02411 02412 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), 02413 User::getGroupsWithPermission( $action ) ); 02414 02415 if ( count( $groups ) ) { 02416 global $wgLang; 02417 return array( 02418 'badaccess-groups', 02419 $wgLang->commaList( $groups ), 02420 count( $groups ) 02421 ); 02422 } else { 02423 return array( 'badaccess-group0' ); 02424 } 02425 } 02426 02438 protected function getUserPermissionsErrorsInternal( $action, $user, 02439 $doExpensiveQueries = true, $short = false 02440 ) { 02441 wfProfileIn( __METHOD__ ); 02442 02443 # Read has special handling 02444 if ( $action == 'read' ) { 02445 $checks = array( 02446 'checkPermissionHooks', 02447 'checkReadPermissions', 02448 ); 02449 # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions 02450 # here as it will lead to duplicate error messages. This is okay to do 02451 # since anywhere that checks for create will also check for edit, and 02452 # those checks are called for edit. 02453 } elseif ( $action == 'create' ) { 02454 $checks = array( 02455 'checkQuickPermissions', 02456 'checkPermissionHooks', 02457 'checkPageRestrictions', 02458 'checkCascadingSourcesRestrictions', 02459 'checkActionPermissions', 02460 'checkUserBlock' 02461 ); 02462 } else { 02463 $checks = array( 02464 'checkQuickPermissions', 02465 'checkPermissionHooks', 02466 'checkSpecialsAndNSPermissions', 02467 'checkCSSandJSPermissions', 02468 'checkPageRestrictions', 02469 'checkCascadingSourcesRestrictions', 02470 'checkActionPermissions', 02471 'checkUserBlock' 02472 ); 02473 } 02474 02475 $errors = array(); 02476 while ( count( $checks ) > 0 && 02477 !( $short && count( $errors ) > 0 ) ) { 02478 $method = array_shift( $checks ); 02479 $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); 02480 } 02481 02482 wfProfileOut( __METHOD__ ); 02483 return $errors; 02484 } 02485 02493 public static function getFilteredRestrictionTypes( $exists = true ) { 02494 global $wgRestrictionTypes; 02495 $types = $wgRestrictionTypes; 02496 if ( $exists ) { 02497 # Remove the create restriction for existing titles 02498 $types = array_diff( $types, array( 'create' ) ); 02499 } else { 02500 # Only the create and upload restrictions apply to non-existing titles 02501 $types = array_intersect( $types, array( 'create', 'upload' ) ); 02502 } 02503 return $types; 02504 } 02505 02511 public function getRestrictionTypes() { 02512 if ( $this->isSpecialPage() ) { 02513 return array(); 02514 } 02515 02516 $types = self::getFilteredRestrictionTypes( $this->exists() ); 02517 02518 if ( $this->getNamespace() != NS_FILE ) { 02519 # Remove the upload restriction for non-file titles 02520 $types = array_diff( $types, array( 'upload' ) ); 02521 } 02522 02523 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); 02524 02525 wfDebug( __METHOD__ . ': applicable restrictions to [[' . 02526 $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); 02527 02528 return $types; 02529 } 02530 02538 private function getTitleProtection() { 02539 // Can't protect pages in special namespaces 02540 if ( $this->getNamespace() < 0 ) { 02541 return false; 02542 } 02543 02544 // Can't protect pages that exist. 02545 if ( $this->exists() ) { 02546 return false; 02547 } 02548 02549 if ( $this->mTitleProtection === null ) { 02550 $dbr = wfGetDB( DB_SLAVE ); 02551 $res = $dbr->select( 02552 'protected_titles', 02553 array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ), 02554 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02555 __METHOD__ 02556 ); 02557 02558 // fetchRow returns false if there are no rows. 02559 $this->mTitleProtection = $dbr->fetchRow( $res ); 02560 } 02561 return $this->mTitleProtection; 02562 } 02563 02567 public function deleteTitleProtection() { 02568 $dbw = wfGetDB( DB_MASTER ); 02569 02570 $dbw->delete( 02571 'protected_titles', 02572 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02573 __METHOD__ 02574 ); 02575 $this->mTitleProtection = false; 02576 } 02577 02585 public function isSemiProtected( $action = 'edit' ) { 02586 global $wgSemiprotectedRestrictionLevels; 02587 02588 $restrictions = $this->getRestrictions( $action ); 02589 $semi = $wgSemiprotectedRestrictionLevels; 02590 if ( !$restrictions || !$semi ) { 02591 // Not protected, or all protection is full protection 02592 return false; 02593 } 02594 02595 // Remap autoconfirmed to editsemiprotected for BC 02596 foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) { 02597 $semi[$key] = 'editsemiprotected'; 02598 } 02599 foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) { 02600 $restrictions[$key] = 'editsemiprotected'; 02601 } 02602 02603 return !array_diff( $restrictions, $semi ); 02604 } 02605 02613 public function isProtected( $action = '' ) { 02614 global $wgRestrictionLevels; 02615 02616 $restrictionTypes = $this->getRestrictionTypes(); 02617 02618 # Special pages have inherent protection 02619 if ( $this->isSpecialPage() ) { 02620 return true; 02621 } 02622 02623 # Check regular protection levels 02624 foreach ( $restrictionTypes as $type ) { 02625 if ( $action == $type || $action == '' ) { 02626 $r = $this->getRestrictions( $type ); 02627 foreach ( $wgRestrictionLevels as $level ) { 02628 if ( in_array( $level, $r ) && $level != '' ) { 02629 return true; 02630 } 02631 } 02632 } 02633 } 02634 02635 return false; 02636 } 02637 02645 public function isNamespaceProtected( User $user ) { 02646 global $wgNamespaceProtection; 02647 02648 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { 02649 foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { 02650 if ( $right != '' && !$user->isAllowed( $right ) ) { 02651 return true; 02652 } 02653 } 02654 } 02655 return false; 02656 } 02657 02663 public function isCascadeProtected() { 02664 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); 02665 return ( $sources > 0 ); 02666 } 02667 02677 public function areCascadeProtectionSourcesLoaded( $getPages = true ) { 02678 return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null; 02679 } 02680 02694 public function getCascadeProtectionSources( $getPages = true ) { 02695 global $wgContLang; 02696 $pagerestrictions = array(); 02697 02698 if ( $this->mCascadeSources !== null && $getPages ) { 02699 return array( $this->mCascadeSources, $this->mCascadingRestrictions ); 02700 } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) { 02701 return array( $this->mHasCascadingRestrictions, $pagerestrictions ); 02702 } 02703 02704 wfProfileIn( __METHOD__ ); 02705 02706 $dbr = wfGetDB( DB_SLAVE ); 02707 02708 if ( $this->getNamespace() == NS_FILE ) { 02709 $tables = array( 'imagelinks', 'page_restrictions' ); 02710 $where_clauses = array( 02711 'il_to' => $this->getDBkey(), 02712 'il_from=pr_page', 02713 'pr_cascade' => 1 02714 ); 02715 } else { 02716 $tables = array( 'templatelinks', 'page_restrictions' ); 02717 $where_clauses = array( 02718 'tl_namespace' => $this->getNamespace(), 02719 'tl_title' => $this->getDBkey(), 02720 'tl_from=pr_page', 02721 'pr_cascade' => 1 02722 ); 02723 } 02724 02725 if ( $getPages ) { 02726 $cols = array( 'pr_page', 'page_namespace', 'page_title', 02727 'pr_expiry', 'pr_type', 'pr_level' ); 02728 $where_clauses[] = 'page_id=pr_page'; 02729 $tables[] = 'page'; 02730 } else { 02731 $cols = array( 'pr_expiry' ); 02732 } 02733 02734 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); 02735 02736 $sources = $getPages ? array() : false; 02737 $now = wfTimestampNow(); 02738 $purgeExpired = false; 02739 02740 foreach ( $res as $row ) { 02741 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02742 if ( $expiry > $now ) { 02743 if ( $getPages ) { 02744 $page_id = $row->pr_page; 02745 $page_ns = $row->page_namespace; 02746 $page_title = $row->page_title; 02747 $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); 02748 # Add groups needed for each restriction type if its not already there 02749 # Make sure this restriction type still exists 02750 02751 if ( !isset( $pagerestrictions[$row->pr_type] ) ) { 02752 $pagerestrictions[$row->pr_type] = array(); 02753 } 02754 02755 if ( 02756 isset( $pagerestrictions[$row->pr_type] ) 02757 && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) 02758 ) { 02759 $pagerestrictions[$row->pr_type][] = $row->pr_level; 02760 } 02761 } else { 02762 $sources = true; 02763 } 02764 } else { 02765 // Trigger lazy purge of expired restrictions from the db 02766 $purgeExpired = true; 02767 } 02768 } 02769 if ( $purgeExpired ) { 02770 Title::purgeExpiredRestrictions(); 02771 } 02772 02773 if ( $getPages ) { 02774 $this->mCascadeSources = $sources; 02775 $this->mCascadingRestrictions = $pagerestrictions; 02776 } else { 02777 $this->mHasCascadingRestrictions = $sources; 02778 } 02779 02780 wfProfileOut( __METHOD__ ); 02781 return array( $sources, $pagerestrictions ); 02782 } 02783 02791 public function areRestrictionsLoaded() { 02792 return $this->mRestrictionsLoaded; 02793 } 02794 02802 public function getRestrictions( $action ) { 02803 if ( !$this->mRestrictionsLoaded ) { 02804 $this->loadRestrictions(); 02805 } 02806 return isset( $this->mRestrictions[$action] ) 02807 ? $this->mRestrictions[$action] 02808 : array(); 02809 } 02810 02818 public function getAllRestrictions() { 02819 if ( !$this->mRestrictionsLoaded ) { 02820 $this->loadRestrictions(); 02821 } 02822 return $this->mRestrictions; 02823 } 02824 02832 public function getRestrictionExpiry( $action ) { 02833 if ( !$this->mRestrictionsLoaded ) { 02834 $this->loadRestrictions(); 02835 } 02836 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; 02837 } 02838 02844 function areRestrictionsCascading() { 02845 if ( !$this->mRestrictionsLoaded ) { 02846 $this->loadRestrictions(); 02847 } 02848 02849 return $this->mCascadeRestriction; 02850 } 02851 02859 private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { 02860 $rows = array(); 02861 02862 foreach ( $res as $row ) { 02863 $rows[] = $row; 02864 } 02865 02866 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); 02867 } 02868 02878 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { 02879 global $wgContLang; 02880 $dbr = wfGetDB( DB_SLAVE ); 02881 02882 $restrictionTypes = $this->getRestrictionTypes(); 02883 02884 foreach ( $restrictionTypes as $type ) { 02885 $this->mRestrictions[$type] = array(); 02886 $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); 02887 } 02888 02889 $this->mCascadeRestriction = false; 02890 02891 # Backwards-compatibility: also load the restrictions from the page record (old format). 02892 02893 if ( $oldFashionedRestrictions === null ) { 02894 $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', 02895 array( 'page_id' => $this->getArticleID() ), __METHOD__ ); 02896 } 02897 02898 if ( $oldFashionedRestrictions != '' ) { 02899 02900 foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { 02901 $temp = explode( '=', trim( $restrict ) ); 02902 if ( count( $temp ) == 1 ) { 02903 // old old format should be treated as edit/move restriction 02904 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); 02905 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); 02906 } else { 02907 $restriction = trim( $temp[1] ); 02908 if ( $restriction != '' ) { //some old entries are empty 02909 $this->mRestrictions[$temp[0]] = explode( ',', $restriction ); 02910 } 02911 } 02912 } 02913 02914 $this->mOldRestrictions = true; 02915 02916 } 02917 02918 if ( count( $rows ) ) { 02919 # Current system - load second to make them override. 02920 $now = wfTimestampNow(); 02921 $purgeExpired = false; 02922 02923 # Cycle through all the restrictions. 02924 foreach ( $rows as $row ) { 02925 02926 // Don't take care of restrictions types that aren't allowed 02927 if ( !in_array( $row->pr_type, $restrictionTypes ) ) { 02928 continue; 02929 } 02930 02931 // This code should be refactored, now that it's being used more generally, 02932 // But I don't really see any harm in leaving it in Block for now -werdna 02933 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02934 02935 // Only apply the restrictions if they haven't expired! 02936 if ( !$expiry || $expiry > $now ) { 02937 $this->mRestrictionsExpiry[$row->pr_type] = $expiry; 02938 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); 02939 02940 $this->mCascadeRestriction |= $row->pr_cascade; 02941 } else { 02942 // Trigger a lazy purge of expired restrictions 02943 $purgeExpired = true; 02944 } 02945 } 02946 02947 if ( $purgeExpired ) { 02948 Title::purgeExpiredRestrictions(); 02949 } 02950 } 02951 02952 $this->mRestrictionsLoaded = true; 02953 } 02954 02961 public function loadRestrictions( $oldFashionedRestrictions = null ) { 02962 global $wgContLang; 02963 if ( !$this->mRestrictionsLoaded ) { 02964 if ( $this->exists() ) { 02965 $dbr = wfGetDB( DB_SLAVE ); 02966 02967 $res = $dbr->select( 02968 'page_restrictions', 02969 array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ), 02970 array( 'pr_page' => $this->getArticleID() ), 02971 __METHOD__ 02972 ); 02973 02974 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); 02975 } else { 02976 $title_protection = $this->getTitleProtection(); 02977 02978 if ( $title_protection ) { 02979 $now = wfTimestampNow(); 02980 $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); 02981 02982 if ( !$expiry || $expiry > $now ) { 02983 // Apply the restrictions 02984 $this->mRestrictionsExpiry['create'] = $expiry; 02985 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); 02986 } else { // Get rid of the old restrictions 02987 Title::purgeExpiredRestrictions(); 02988 $this->mTitleProtection = false; 02989 } 02990 } else { 02991 $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); 02992 } 02993 $this->mRestrictionsLoaded = true; 02994 } 02995 } 02996 } 02997 03002 public function flushRestrictions() { 03003 $this->mRestrictionsLoaded = false; 03004 $this->mTitleProtection = null; 03005 } 03006 03010 static function purgeExpiredRestrictions() { 03011 if ( wfReadOnly() ) { 03012 return; 03013 } 03014 03015 $method = __METHOD__; 03016 $dbw = wfGetDB( DB_MASTER ); 03017 $dbw->onTransactionIdle( function () use ( $dbw, $method ) { 03018 $dbw->delete( 03019 'page_restrictions', 03020 array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 03021 $method 03022 ); 03023 $dbw->delete( 03024 'protected_titles', 03025 array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 03026 $method 03027 ); 03028 } ); 03029 } 03030 03036 public function hasSubpages() { 03037 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 03038 # Duh 03039 return false; 03040 } 03041 03042 # We dynamically add a member variable for the purpose of this method 03043 # alone to cache the result. There's no point in having it hanging 03044 # around uninitialized in every Title object; therefore we only add it 03045 # if needed and don't declare it statically. 03046 if ( $this->mHasSubpages === null ) { 03047 $this->mHasSubpages = false; 03048 $subpages = $this->getSubpages( 1 ); 03049 if ( $subpages instanceof TitleArray ) { 03050 $this->mHasSubpages = (bool)$subpages->count(); 03051 } 03052 } 03053 03054 return $this->mHasSubpages; 03055 } 03056 03064 public function getSubpages( $limit = -1 ) { 03065 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03066 return array(); 03067 } 03068 03069 $dbr = wfGetDB( DB_SLAVE ); 03070 $conds['page_namespace'] = $this->getNamespace(); 03071 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); 03072 $options = array(); 03073 if ( $limit > -1 ) { 03074 $options['LIMIT'] = $limit; 03075 } 03076 $this->mSubpages = TitleArray::newFromResult( 03077 $dbr->select( 'page', 03078 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), 03079 $conds, 03080 __METHOD__, 03081 $options 03082 ) 03083 ); 03084 return $this->mSubpages; 03085 } 03086 03092 public function isDeleted() { 03093 if ( $this->getNamespace() < 0 ) { 03094 $n = 0; 03095 } else { 03096 $dbr = wfGetDB( DB_SLAVE ); 03097 03098 $n = $dbr->selectField( 'archive', 'COUNT(*)', 03099 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 03100 __METHOD__ 03101 ); 03102 if ( $this->getNamespace() == NS_FILE ) { 03103 $n += $dbr->selectField( 'filearchive', 'COUNT(*)', 03104 array( 'fa_name' => $this->getDBkey() ), 03105 __METHOD__ 03106 ); 03107 } 03108 } 03109 return (int)$n; 03110 } 03111 03117 public function isDeletedQuick() { 03118 if ( $this->getNamespace() < 0 ) { 03119 return false; 03120 } 03121 $dbr = wfGetDB( DB_SLAVE ); 03122 $deleted = (bool)$dbr->selectField( 'archive', '1', 03123 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 03124 __METHOD__ 03125 ); 03126 if ( !$deleted && $this->getNamespace() == NS_FILE ) { 03127 $deleted = (bool)$dbr->selectField( 'filearchive', '1', 03128 array( 'fa_name' => $this->getDBkey() ), 03129 __METHOD__ 03130 ); 03131 } 03132 return $deleted; 03133 } 03134 03143 public function getArticleID( $flags = 0 ) { 03144 if ( $this->getNamespace() < 0 ) { 03145 $this->mArticleID = 0; 03146 return $this->mArticleID; 03147 } 03148 $linkCache = LinkCache::singleton(); 03149 if ( $flags & self::GAID_FOR_UPDATE ) { 03150 $oldUpdate = $linkCache->forUpdate( true ); 03151 $linkCache->clearLink( $this ); 03152 $this->mArticleID = $linkCache->addLinkObj( $this ); 03153 $linkCache->forUpdate( $oldUpdate ); 03154 } else { 03155 if ( -1 == $this->mArticleID ) { 03156 $this->mArticleID = $linkCache->addLinkObj( $this ); 03157 } 03158 } 03159 return $this->mArticleID; 03160 } 03161 03169 public function isRedirect( $flags = 0 ) { 03170 if ( !is_null( $this->mRedirect ) ) { 03171 return $this->mRedirect; 03172 } 03173 # Calling getArticleID() loads the field from cache as needed 03174 if ( !$this->getArticleID( $flags ) ) { 03175 $this->mRedirect = false; 03176 return $this->mRedirect; 03177 } 03178 03179 $linkCache = LinkCache::singleton(); 03180 $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' ); 03181 if ( $cached === null ) { 03182 # Trust LinkCache's state over our own 03183 # LinkCache is telling us that the page doesn't exist, despite there being cached 03184 # data relating to an existing page in $this->mArticleID. Updaters should clear 03185 # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is 03186 # set, then LinkCache will definitely be up to date here, since getArticleID() forces 03187 # LinkCache to refresh its data from the master. 03188 $this->mRedirect = false; 03189 return $this->mRedirect; 03190 } 03191 03192 $this->mRedirect = (bool)$cached; 03193 03194 return $this->mRedirect; 03195 } 03196 03204 public function getLength( $flags = 0 ) { 03205 if ( $this->mLength != -1 ) { 03206 return $this->mLength; 03207 } 03208 # Calling getArticleID() loads the field from cache as needed 03209 if ( !$this->getArticleID( $flags ) ) { 03210 $this->mLength = 0; 03211 return $this->mLength; 03212 } 03213 $linkCache = LinkCache::singleton(); 03214 $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' ); 03215 if ( $cached === null ) { 03216 # Trust LinkCache's state over our own, as for isRedirect() 03217 $this->mLength = 0; 03218 return $this->mLength; 03219 } 03220 03221 $this->mLength = intval( $cached ); 03222 03223 return $this->mLength; 03224 } 03225 03232 public function getLatestRevID( $flags = 0 ) { 03233 if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) { 03234 return intval( $this->mLatestID ); 03235 } 03236 # Calling getArticleID() loads the field from cache as needed 03237 if ( !$this->getArticleID( $flags ) ) { 03238 $this->mLatestID = 0; 03239 return $this->mLatestID; 03240 } 03241 $linkCache = LinkCache::singleton(); 03242 $linkCache->addLinkObj( $this ); 03243 $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' ); 03244 if ( $cached === null ) { 03245 # Trust LinkCache's state over our own, as for isRedirect() 03246 $this->mLatestID = 0; 03247 return $this->mLatestID; 03248 } 03249 03250 $this->mLatestID = intval( $cached ); 03251 03252 return $this->mLatestID; 03253 } 03254 03265 public function resetArticleID( $newid ) { 03266 $linkCache = LinkCache::singleton(); 03267 $linkCache->clearLink( $this ); 03268 03269 if ( $newid === false ) { 03270 $this->mArticleID = -1; 03271 } else { 03272 $this->mArticleID = intval( $newid ); 03273 } 03274 $this->mRestrictionsLoaded = false; 03275 $this->mRestrictions = array(); 03276 $this->mRedirect = null; 03277 $this->mLength = -1; 03278 $this->mLatestID = false; 03279 $this->mContentModel = false; 03280 $this->mEstimateRevisions = null; 03281 $this->mPageLanguage = false; 03282 $this->mDbPageLanguage = null; 03283 $this->mIsBigDeletion = null; 03284 } 03285 03293 public static function capitalize( $text, $ns = NS_MAIN ) { 03294 global $wgContLang; 03295 03296 if ( MWNamespace::isCapitalized( $ns ) ) { 03297 return $wgContLang->ucfirst( $text ); 03298 } else { 03299 return $text; 03300 } 03301 } 03302 03314 private function secureAndSplit() { 03315 # Initialisation 03316 $this->mInterwiki = ''; 03317 $this->mFragment = ''; 03318 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN 03319 03320 $dbkey = $this->mDbkeyform; 03321 03322 try { 03323 // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share 03324 // the parsing code with Title, while avoiding massive refactoring. 03325 // @todo: get rid of secureAndSplit, refactor parsing code. 03326 $titleParser = self::getTitleParser(); 03327 $parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() ); 03328 } catch ( MalformedTitleException $ex ) { 03329 return false; 03330 } 03331 03332 # Fill fields 03333 $this->setFragment( '#' . $parts['fragment'] ); 03334 $this->mInterwiki = $parts['interwiki']; 03335 $this->mLocalInterwiki = $parts['local_interwiki']; 03336 $this->mNamespace = $parts['namespace']; 03337 $this->mUserCaseDBKey = $parts['user_case_dbkey']; 03338 03339 $this->mDbkeyform = $parts['dbkey']; 03340 $this->mUrlform = wfUrlencode( $this->mDbkeyform ); 03341 $this->mTextform = str_replace( '_', ' ', $this->mDbkeyform ); 03342 03343 # We already know that some pages won't be in the database! 03344 if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) { 03345 $this->mArticleID = 0; 03346 } 03347 03348 return true; 03349 } 03350 03363 public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03364 if ( count( $options ) > 0 ) { 03365 $db = wfGetDB( DB_MASTER ); 03366 } else { 03367 $db = wfGetDB( DB_SLAVE ); 03368 } 03369 03370 $res = $db->select( 03371 array( 'page', $table ), 03372 self::getSelectFields(), 03373 array( 03374 "{$prefix}_from=page_id", 03375 "{$prefix}_namespace" => $this->getNamespace(), 03376 "{$prefix}_title" => $this->getDBkey() ), 03377 __METHOD__, 03378 $options 03379 ); 03380 03381 $retVal = array(); 03382 if ( $res->numRows() ) { 03383 $linkCache = LinkCache::singleton(); 03384 foreach ( $res as $row ) { 03385 $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ); 03386 if ( $titleObj ) { 03387 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03388 $retVal[] = $titleObj; 03389 } 03390 } 03391 } 03392 return $retVal; 03393 } 03394 03405 public function getTemplateLinksTo( $options = array() ) { 03406 return $this->getLinksTo( $options, 'templatelinks', 'tl' ); 03407 } 03408 03421 public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03422 global $wgContentHandlerUseDB; 03423 03424 $id = $this->getArticleID(); 03425 03426 # If the page doesn't exist; there can't be any link from this page 03427 if ( !$id ) { 03428 return array(); 03429 } 03430 03431 if ( count( $options ) > 0 ) { 03432 $db = wfGetDB( DB_MASTER ); 03433 } else { 03434 $db = wfGetDB( DB_SLAVE ); 03435 } 03436 03437 $namespaceFiled = "{$prefix}_namespace"; 03438 $titleField = "{$prefix}_title"; 03439 03440 $fields = array( 03441 $namespaceFiled, 03442 $titleField, 03443 'page_id', 03444 'page_len', 03445 'page_is_redirect', 03446 'page_latest' 03447 ); 03448 03449 if ( $wgContentHandlerUseDB ) { 03450 $fields[] = 'page_content_model'; 03451 } 03452 03453 $res = $db->select( 03454 array( $table, 'page' ), 03455 $fields, 03456 array( "{$prefix}_from" => $id ), 03457 __METHOD__, 03458 $options, 03459 array( 'page' => array( 03460 'LEFT JOIN', 03461 array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) 03462 ) ) 03463 ); 03464 03465 $retVal = array(); 03466 if ( $res->numRows() ) { 03467 $linkCache = LinkCache::singleton(); 03468 foreach ( $res as $row ) { 03469 $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); 03470 if ( $titleObj ) { 03471 if ( $row->page_id ) { 03472 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03473 } else { 03474 $linkCache->addBadLinkObj( $titleObj ); 03475 } 03476 $retVal[] = $titleObj; 03477 } 03478 } 03479 } 03480 return $retVal; 03481 } 03482 03493 public function getTemplateLinksFrom( $options = array() ) { 03494 return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); 03495 } 03496 03505 public function getBrokenLinksFrom() { 03506 if ( $this->getArticleID() == 0 ) { 03507 # All links from article ID 0 are false positives 03508 return array(); 03509 } 03510 03511 $dbr = wfGetDB( DB_SLAVE ); 03512 $res = $dbr->select( 03513 array( 'page', 'pagelinks' ), 03514 array( 'pl_namespace', 'pl_title' ), 03515 array( 03516 'pl_from' => $this->getArticleID(), 03517 'page_namespace IS NULL' 03518 ), 03519 __METHOD__, array(), 03520 array( 03521 'page' => array( 03522 'LEFT JOIN', 03523 array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) 03524 ) 03525 ) 03526 ); 03527 03528 $retVal = array(); 03529 foreach ( $res as $row ) { 03530 $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); 03531 } 03532 return $retVal; 03533 } 03534 03541 public function getSquidURLs() { 03542 $urls = array( 03543 $this->getInternalURL(), 03544 $this->getInternalURL( 'action=history' ) 03545 ); 03546 03547 $pageLang = $this->getPageLanguage(); 03548 if ( $pageLang->hasVariants() ) { 03549 $variants = $pageLang->getVariants(); 03550 foreach ( $variants as $vCode ) { 03551 $urls[] = $this->getInternalURL( '', $vCode ); 03552 } 03553 } 03554 03555 // If we are looking at a css/js user subpage, purge the action=raw. 03556 if ( $this->isJsSubpage() ) { 03557 $urls[] = $this->getInternalUrl( 'action=raw&ctype=text/javascript' ); 03558 } elseif ( $this->isCssSubpage() ) { 03559 $urls[] = $this->getInternalUrl( 'action=raw&ctype=text/css' ); 03560 } 03561 03562 wfRunHooks( 'TitleSquidURLs', array( $this, &$urls ) ); 03563 return $urls; 03564 } 03565 03569 public function purgeSquid() { 03570 global $wgUseSquid; 03571 if ( $wgUseSquid ) { 03572 $urls = $this->getSquidURLs(); 03573 $u = new SquidUpdate( $urls ); 03574 $u->doUpdate(); 03575 } 03576 } 03577 03584 public function moveNoAuth( &$nt ) { 03585 return $this->moveTo( $nt, false ); 03586 } 03587 03599 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { 03600 global $wgUser, $wgContentHandlerUseDB; 03601 03602 $errors = array(); 03603 if ( !$nt ) { 03604 // Normally we'd add this to $errors, but we'll get 03605 // lots of syntax errors if $nt is not an object 03606 return array( array( 'badtitletext' ) ); 03607 } 03608 if ( $this->equals( $nt ) ) { 03609 $errors[] = array( 'selfmove' ); 03610 } 03611 if ( !$this->isMovable() ) { 03612 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 03613 } 03614 if ( $nt->isExternal() ) { 03615 $errors[] = array( 'immobile-target-namespace-iw' ); 03616 } 03617 if ( !$nt->isMovable() ) { 03618 $errors[] = array( 'immobile-target-namespace', $nt->getNsText() ); 03619 } 03620 03621 $oldid = $this->getArticleID(); 03622 $newid = $nt->getArticleID(); 03623 03624 if ( strlen( $nt->getDBkey() ) < 1 ) { 03625 $errors[] = array( 'articleexists' ); 03626 } 03627 if ( 03628 ( $this->getDBkey() == '' ) || 03629 ( !$oldid ) || 03630 ( $nt->getDBkey() == '' ) 03631 ) { 03632 $errors[] = array( 'badarticleerror' ); 03633 } 03634 03635 // Content model checks 03636 if ( !$wgContentHandlerUseDB && 03637 $this->getContentModel() !== $nt->getContentModel() ) { 03638 // can't move a page if that would change the page's content model 03639 $errors[] = array( 03640 'bad-target-model', 03641 ContentHandler::getLocalizedName( $this->getContentModel() ), 03642 ContentHandler::getLocalizedName( $nt->getContentModel() ) 03643 ); 03644 } 03645 03646 // Image-specific checks 03647 if ( $this->getNamespace() == NS_FILE ) { 03648 $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) ); 03649 } 03650 03651 if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) { 03652 $errors[] = array( 'nonfile-cannot-move-to-file' ); 03653 } 03654 03655 if ( $auth ) { 03656 $errors = wfMergeErrorArrays( $errors, 03657 $this->getUserPermissionsErrors( 'move', $wgUser ), 03658 $this->getUserPermissionsErrors( 'edit', $wgUser ), 03659 $nt->getUserPermissionsErrors( 'move-target', $wgUser ), 03660 $nt->getUserPermissionsErrors( 'edit', $wgUser ) ); 03661 } 03662 03663 $match = EditPage::matchSummarySpamRegex( $reason ); 03664 if ( $match !== false ) { 03665 // This is kind of lame, won't display nice 03666 $errors[] = array( 'spamprotectiontext' ); 03667 } 03668 03669 $err = null; 03670 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) { 03671 $errors[] = array( 'hookaborted', $err ); 03672 } 03673 03674 # The move is allowed only if (1) the target doesn't exist, or 03675 # (2) the target is a redirect to the source, and has no history 03676 # (so we can undo bad moves right after they're done). 03677 03678 if ( 0 != $newid ) { # Target exists; check for validity 03679 if ( !$this->isValidMoveTarget( $nt ) ) { 03680 $errors[] = array( 'articleexists' ); 03681 } 03682 } else { 03683 $tp = $nt->getTitleProtection(); 03684 $right = $tp['pt_create_perm']; 03685 if ( $right == 'sysop' ) { 03686 $right = 'editprotected'; // B/C 03687 } 03688 if ( $right == 'autoconfirmed' ) { 03689 $right = 'editsemiprotected'; // B/C 03690 } 03691 if ( $tp and !$wgUser->isAllowed( $right ) ) { 03692 $errors[] = array( 'cantmove-titleprotected' ); 03693 } 03694 } 03695 if ( empty( $errors ) ) { 03696 return true; 03697 } 03698 return $errors; 03699 } 03700 03706 protected function validateFileMoveOperation( $nt ) { 03707 global $wgUser; 03708 03709 $errors = array(); 03710 03711 // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below 03712 03713 $file = wfLocalFile( $this ); 03714 if ( $file->exists() ) { 03715 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { 03716 $errors[] = array( 'imageinvalidfilename' ); 03717 } 03718 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) { 03719 $errors[] = array( 'imagetypemismatch' ); 03720 } 03721 } 03722 03723 if ( $nt->getNamespace() != NS_FILE ) { 03724 $errors[] = array( 'imagenocrossnamespace' ); 03725 // From here we want to do checks on a file object, so if we can't 03726 // create one, we must return. 03727 return $errors; 03728 } 03729 03730 // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here 03731 03732 $destFile = wfLocalFile( $nt ); 03733 if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) { 03734 $errors[] = array( 'file-exists-sharedrepo' ); 03735 } 03736 03737 return $errors; 03738 } 03739 03752 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { 03753 global $wgUser; 03754 $err = $this->isValidMoveOperation( $nt, $auth, $reason ); 03755 if ( is_array( $err ) ) { 03756 // Auto-block user's IP if the account was "hard" blocked 03757 $wgUser->spreadAnyEditBlock(); 03758 return $err; 03759 } 03760 // Check suppressredirect permission 03761 if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) { 03762 $createRedirect = true; 03763 } 03764 03765 wfRunHooks( 'TitleMove', array( $this, $nt, $wgUser ) ); 03766 03767 $mp = new MovePage( $this, $nt ); 03768 $status = $mp->move( $wgUser, $reason, $createRedirect ); 03769 if ( $status->isOK() ) { 03770 return true; 03771 } else { 03772 return $status->getErrorsArray(); 03773 } 03774 } 03775 03788 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { 03789 global $wgMaximumMovedPages; 03790 // Check permissions 03791 if ( !$this->userCan( 'move-subpages' ) ) { 03792 return array( 'cant-move-subpages' ); 03793 } 03794 // Do the source and target namespaces support subpages? 03795 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03796 return array( 'namespace-nosubpages', 03797 MWNamespace::getCanonicalName( $this->getNamespace() ) ); 03798 } 03799 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) { 03800 return array( 'namespace-nosubpages', 03801 MWNamespace::getCanonicalName( $nt->getNamespace() ) ); 03802 } 03803 03804 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 ); 03805 $retval = array(); 03806 $count = 0; 03807 foreach ( $subpages as $oldSubpage ) { 03808 $count++; 03809 if ( $count > $wgMaximumMovedPages ) { 03810 $retval[$oldSubpage->getPrefixedText()] = 03811 array( 'movepage-max-pages', 03812 $wgMaximumMovedPages ); 03813 break; 03814 } 03815 03816 // We don't know whether this function was called before 03817 // or after moving the root page, so check both 03818 // $this and $nt 03819 if ( $oldSubpage->getArticleID() == $this->getArticleID() 03820 || $oldSubpage->getArticleID() == $nt->getArticleID() 03821 ) { 03822 // When moving a page to a subpage of itself, 03823 // don't move it twice 03824 continue; 03825 } 03826 $newPageName = preg_replace( 03827 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#', 03828 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 03829 $oldSubpage->getDBkey() ); 03830 if ( $oldSubpage->isTalkPage() ) { 03831 $newNs = $nt->getTalkPage()->getNamespace(); 03832 } else { 03833 $newNs = $nt->getSubjectPage()->getNamespace(); 03834 } 03835 # Bug 14385: we need makeTitleSafe because the new page names may 03836 # be longer than 255 characters. 03837 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); 03838 03839 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); 03840 if ( $success === true ) { 03841 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); 03842 } else { 03843 $retval[$oldSubpage->getPrefixedText()] = $success; 03844 } 03845 } 03846 return $retval; 03847 } 03848 03855 public function isSingleRevRedirect() { 03856 global $wgContentHandlerUseDB; 03857 03858 $dbw = wfGetDB( DB_MASTER ); 03859 03860 # Is it a redirect? 03861 $fields = array( 'page_is_redirect', 'page_latest', 'page_id' ); 03862 if ( $wgContentHandlerUseDB ) { 03863 $fields[] = 'page_content_model'; 03864 } 03865 03866 $row = $dbw->selectRow( 'page', 03867 $fields, 03868 $this->pageCond(), 03869 __METHOD__, 03870 array( 'FOR UPDATE' ) 03871 ); 03872 # Cache some fields we may want 03873 $this->mArticleID = $row ? intval( $row->page_id ) : 0; 03874 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; 03875 $this->mLatestID = $row ? intval( $row->page_latest ) : false; 03876 $this->mContentModel = $row && isset( $row->page_content_model ) 03877 ? strval( $row->page_content_model ) 03878 : false; 03879 03880 if ( !$this->mRedirect ) { 03881 return false; 03882 } 03883 # Does the article have a history? 03884 $row = $dbw->selectField( array( 'page', 'revision' ), 03885 'rev_id', 03886 array( 'page_namespace' => $this->getNamespace(), 03887 'page_title' => $this->getDBkey(), 03888 'page_id=rev_page', 03889 'page_latest != rev_id' 03890 ), 03891 __METHOD__, 03892 array( 'FOR UPDATE' ) 03893 ); 03894 # Return true if there was no history 03895 return ( $row === false ); 03896 } 03897 03905 public function isValidMoveTarget( $nt ) { 03906 # Is it an existing file? 03907 if ( $nt->getNamespace() == NS_FILE ) { 03908 $file = wfLocalFile( $nt ); 03909 if ( $file->exists() ) { 03910 wfDebug( __METHOD__ . ": file exists\n" ); 03911 return false; 03912 } 03913 } 03914 # Is it a redirect with no history? 03915 if ( !$nt->isSingleRevRedirect() ) { 03916 wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); 03917 return false; 03918 } 03919 # Get the article text 03920 $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST ); 03921 if ( !is_object( $rev ) ) { 03922 return false; 03923 } 03924 $content = $rev->getContent(); 03925 # Does the redirect point to the source? 03926 # Or is it a broken self-redirect, usually caused by namespace collisions? 03927 $redirTitle = $content ? $content->getRedirectTarget() : null; 03928 03929 if ( $redirTitle ) { 03930 if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && 03931 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) { 03932 wfDebug( __METHOD__ . ": redirect points to other page\n" ); 03933 return false; 03934 } else { 03935 return true; 03936 } 03937 } else { 03938 # Fail safe (not a redirect after all. strange.) 03939 wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . 03940 " is a redirect, but it doesn't contain a valid redirect.\n" ); 03941 return false; 03942 } 03943 } 03944 03952 public function getParentCategories() { 03953 global $wgContLang; 03954 03955 $data = array(); 03956 03957 $titleKey = $this->getArticleID(); 03958 03959 if ( $titleKey === 0 ) { 03960 return $data; 03961 } 03962 03963 $dbr = wfGetDB( DB_SLAVE ); 03964 03965 $res = $dbr->select( 03966 'categorylinks', 03967 'cl_to', 03968 array( 'cl_from' => $titleKey ), 03969 __METHOD__ 03970 ); 03971 03972 if ( $res->numRows() > 0 ) { 03973 foreach ( $res as $row ) { 03974 // $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to); 03975 $data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText(); 03976 } 03977 } 03978 return $data; 03979 } 03980 03987 public function getParentCategoryTree( $children = array() ) { 03988 $stack = array(); 03989 $parents = $this->getParentCategories(); 03990 03991 if ( $parents ) { 03992 foreach ( $parents as $parent => $current ) { 03993 if ( array_key_exists( $parent, $children ) ) { 03994 # Circular reference 03995 $stack[$parent] = array(); 03996 } else { 03997 $nt = Title::newFromText( $parent ); 03998 if ( $nt ) { 03999 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) ); 04000 } 04001 } 04002 } 04003 } 04004 04005 return $stack; 04006 } 04007 04014 public function pageCond() { 04015 if ( $this->mArticleID > 0 ) { 04016 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs 04017 return array( 'page_id' => $this->mArticleID ); 04018 } else { 04019 return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); 04020 } 04021 } 04022 04030 public function getPreviousRevisionID( $revId, $flags = 0 ) { 04031 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04032 $revId = $db->selectField( 'revision', 'rev_id', 04033 array( 04034 'rev_page' => $this->getArticleID( $flags ), 04035 'rev_id < ' . intval( $revId ) 04036 ), 04037 __METHOD__, 04038 array( 'ORDER BY' => 'rev_id DESC' ) 04039 ); 04040 04041 if ( $revId === false ) { 04042 return false; 04043 } else { 04044 return intval( $revId ); 04045 } 04046 } 04047 04055 public function getNextRevisionID( $revId, $flags = 0 ) { 04056 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04057 $revId = $db->selectField( 'revision', 'rev_id', 04058 array( 04059 'rev_page' => $this->getArticleID( $flags ), 04060 'rev_id > ' . intval( $revId ) 04061 ), 04062 __METHOD__, 04063 array( 'ORDER BY' => 'rev_id' ) 04064 ); 04065 04066 if ( $revId === false ) { 04067 return false; 04068 } else { 04069 return intval( $revId ); 04070 } 04071 } 04072 04079 public function getFirstRevision( $flags = 0 ) { 04080 $pageId = $this->getArticleID( $flags ); 04081 if ( $pageId ) { 04082 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 04083 $row = $db->selectRow( 'revision', Revision::selectFields(), 04084 array( 'rev_page' => $pageId ), 04085 __METHOD__, 04086 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) 04087 ); 04088 if ( $row ) { 04089 return new Revision( $row ); 04090 } 04091 } 04092 return null; 04093 } 04094 04101 public function getEarliestRevTime( $flags = 0 ) { 04102 $rev = $this->getFirstRevision( $flags ); 04103 return $rev ? $rev->getTimestamp() : null; 04104 } 04105 04111 public function isNewPage() { 04112 $dbr = wfGetDB( DB_SLAVE ); 04113 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); 04114 } 04115 04121 public function isBigDeletion() { 04122 global $wgDeleteRevisionsLimit; 04123 04124 if ( !$wgDeleteRevisionsLimit ) { 04125 return false; 04126 } 04127 04128 if ( $this->mIsBigDeletion === null ) { 04129 $dbr = wfGetDB( DB_SLAVE ); 04130 04131 $innerQuery = $dbr->selectSQLText( 04132 'revision', 04133 '1', 04134 array( 'rev_page' => $this->getArticleID() ), 04135 __METHOD__, 04136 array( 'LIMIT' => $wgDeleteRevisionsLimit + 1 ) 04137 ); 04138 04139 $revCount = $dbr->query( 04140 'SELECT COUNT(*) FROM (' . $innerQuery . ') AS innerQuery', 04141 __METHOD__ 04142 ); 04143 $revCount = $revCount->fetchRow(); 04144 $revCount = $revCount['COUNT(*)']; 04145 04146 $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit; 04147 } 04148 04149 return $this->mIsBigDeletion; 04150 } 04151 04157 public function estimateRevisionCount() { 04158 if ( !$this->exists() ) { 04159 return 0; 04160 } 04161 04162 if ( $this->mEstimateRevisions === null ) { 04163 $dbr = wfGetDB( DB_SLAVE ); 04164 $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', 04165 array( 'rev_page' => $this->getArticleID() ), __METHOD__ ); 04166 } 04167 04168 return $this->mEstimateRevisions; 04169 } 04170 04180 public function countRevisionsBetween( $old, $new, $max = null ) { 04181 if ( !( $old instanceof Revision ) ) { 04182 $old = Revision::newFromTitle( $this, (int)$old ); 04183 } 04184 if ( !( $new instanceof Revision ) ) { 04185 $new = Revision::newFromTitle( $this, (int)$new ); 04186 } 04187 if ( !$old || !$new ) { 04188 return 0; // nothing to compare 04189 } 04190 $dbr = wfGetDB( DB_SLAVE ); 04191 $conds = array( 04192 'rev_page' => $this->getArticleID(), 04193 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04194 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04195 ); 04196 if ( $max !== null ) { 04197 $res = $dbr->select( 'revision', '1', 04198 $conds, 04199 __METHOD__, 04200 array( 'LIMIT' => $max + 1 ) // extra to detect truncation 04201 ); 04202 return $res->numRows(); 04203 } else { 04204 return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ ); 04205 } 04206 } 04207 04224 public function getAuthorsBetween( $old, $new, $limit, $options = array() ) { 04225 if ( !( $old instanceof Revision ) ) { 04226 $old = Revision::newFromTitle( $this, (int)$old ); 04227 } 04228 if ( !( $new instanceof Revision ) ) { 04229 $new = Revision::newFromTitle( $this, (int)$new ); 04230 } 04231 // XXX: what if Revision objects are passed in, but they don't refer to this title? 04232 // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID() 04233 // in the sanity check below? 04234 if ( !$old || !$new ) { 04235 return null; // nothing to compare 04236 } 04237 $authors = array(); 04238 $old_cmp = '>'; 04239 $new_cmp = '<'; 04240 $options = (array)$options; 04241 if ( in_array( 'include_old', $options ) ) { 04242 $old_cmp = '>='; 04243 } 04244 if ( in_array( 'include_new', $options ) ) { 04245 $new_cmp = '<='; 04246 } 04247 if ( in_array( 'include_both', $options ) ) { 04248 $old_cmp = '>='; 04249 $new_cmp = '<='; 04250 } 04251 // No DB query needed if $old and $new are the same or successive revisions: 04252 if ( $old->getId() === $new->getId() ) { 04253 return ( $old_cmp === '>' && $new_cmp === '<' ) ? array() : array( $old->getRawUserText() ); 04254 } elseif ( $old->getId() === $new->getParentId() ) { 04255 if ( $old_cmp === '>=' && $new_cmp === '<=' ) { 04256 $authors[] = $old->getRawUserText(); 04257 if ( $old->getRawUserText() != $new->getRawUserText() ) { 04258 $authors[] = $new->getRawUserText(); 04259 } 04260 } elseif ( $old_cmp === '>=' ) { 04261 $authors[] = $old->getRawUserText(); 04262 } elseif ( $new_cmp === '<=' ) { 04263 $authors[] = $new->getRawUserText(); 04264 } 04265 return $authors; 04266 } 04267 $dbr = wfGetDB( DB_SLAVE ); 04268 $res = $dbr->select( 'revision', 'DISTINCT rev_user_text', 04269 array( 04270 'rev_page' => $this->getArticleID(), 04271 "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04272 "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04273 ), __METHOD__, 04274 array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated 04275 ); 04276 foreach ( $res as $row ) { 04277 $authors[] = $row->rev_user_text; 04278 } 04279 return $authors; 04280 } 04281 04296 public function countAuthorsBetween( $old, $new, $limit, $options = array() ) { 04297 $authors = $this->getAuthorsBetween( $old, $new, $limit, $options ); 04298 return $authors ? count( $authors ) : 0; 04299 } 04300 04307 public function equals( Title $title ) { 04308 // Note: === is necessary for proper matching of number-like titles. 04309 return $this->getInterwiki() === $title->getInterwiki() 04310 && $this->getNamespace() == $title->getNamespace() 04311 && $this->getDBkey() === $title->getDBkey(); 04312 } 04313 04320 public function isSubpageOf( Title $title ) { 04321 return $this->getInterwiki() === $title->getInterwiki() 04322 && $this->getNamespace() == $title->getNamespace() 04323 && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; 04324 } 04325 04335 public function exists() { 04336 $exists = $this->getArticleID() != 0; 04337 wfRunHooks( 'TitleExists', array( $this, &$exists ) ); 04338 return $exists; 04339 } 04340 04357 public function isAlwaysKnown() { 04358 $isKnown = null; 04359 04370 wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) ); 04371 04372 if ( !is_null( $isKnown ) ) { 04373 return $isKnown; 04374 } 04375 04376 if ( $this->isExternal() ) { 04377 return true; // any interwiki link might be viewable, for all we know 04378 } 04379 04380 switch ( $this->mNamespace ) { 04381 case NS_MEDIA: 04382 case NS_FILE: 04383 // file exists, possibly in a foreign repo 04384 return (bool)wfFindFile( $this ); 04385 case NS_SPECIAL: 04386 // valid special page 04387 return SpecialPageFactory::exists( $this->getDBkey() ); 04388 case NS_MAIN: 04389 // selflink, possibly with fragment 04390 return $this->mDbkeyform == ''; 04391 case NS_MEDIAWIKI: 04392 // known system message 04393 return $this->hasSourceText() !== false; 04394 default: 04395 return false; 04396 } 04397 } 04398 04410 public function isKnown() { 04411 return $this->isAlwaysKnown() || $this->exists(); 04412 } 04413 04419 public function hasSourceText() { 04420 if ( $this->exists() ) { 04421 return true; 04422 } 04423 04424 if ( $this->mNamespace == NS_MEDIAWIKI ) { 04425 // If the page doesn't exist but is a known system message, default 04426 // message content will be displayed, same for language subpages- 04427 // Use always content language to avoid loading hundreds of languages 04428 // to get the link color. 04429 global $wgContLang; 04430 list( $name, ) = MessageCache::singleton()->figureMessage( 04431 $wgContLang->lcfirst( $this->getText() ) 04432 ); 04433 $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false ); 04434 return $message->exists(); 04435 } 04436 04437 return false; 04438 } 04439 04445 public function getDefaultMessageText() { 04446 global $wgContLang; 04447 04448 if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case 04449 return false; 04450 } 04451 04452 list( $name, $lang ) = MessageCache::singleton()->figureMessage( 04453 $wgContLang->lcfirst( $this->getText() ) 04454 ); 04455 $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false ); 04456 04457 if ( $message->exists() ) { 04458 return $message->plain(); 04459 } else { 04460 return false; 04461 } 04462 } 04463 04469 public function invalidateCache() { 04470 if ( wfReadOnly() ) { 04471 return false; 04472 } 04473 04474 if ( $this->mArticleID === 0 ) { 04475 return true; // avoid gap locking if we know it's not there 04476 } 04477 04478 $method = __METHOD__; 04479 $dbw = wfGetDB( DB_MASTER ); 04480 $conds = $this->pageCond(); 04481 $dbw->onTransactionIdle( function () use ( $dbw, $conds, $method ) { 04482 $dbw->update( 04483 'page', 04484 array( 'page_touched' => $dbw->timestamp() ), 04485 $conds, 04486 $method 04487 ); 04488 } ); 04489 04490 return true; 04491 } 04492 04498 public function touchLinks() { 04499 $u = new HTMLCacheUpdate( $this, 'pagelinks' ); 04500 $u->doUpdate(); 04501 04502 if ( $this->getNamespace() == NS_CATEGORY ) { 04503 $u = new HTMLCacheUpdate( $this, 'categorylinks' ); 04504 $u->doUpdate(); 04505 } 04506 } 04507 04514 public function getTouched( $db = null ) { 04515 if ( $db === null ) { 04516 $db = wfGetDB( DB_SLAVE ); 04517 } 04518 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); 04519 return $touched; 04520 } 04521 04528 public function getNotificationTimestamp( $user = null ) { 04529 global $wgUser, $wgShowUpdatedMarker; 04530 // Assume current user if none given 04531 if ( !$user ) { 04532 $user = $wgUser; 04533 } 04534 // Check cache first 04535 $uid = $user->getId(); 04536 // avoid isset here, as it'll return false for null entries 04537 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) { 04538 return $this->mNotificationTimestamp[$uid]; 04539 } 04540 if ( !$uid || !$wgShowUpdatedMarker || !$user->isAllowed( 'viewmywatchlist' ) ) { 04541 $this->mNotificationTimestamp[$uid] = false; 04542 return $this->mNotificationTimestamp[$uid]; 04543 } 04544 // Don't cache too much! 04545 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) { 04546 $this->mNotificationTimestamp = array(); 04547 } 04548 $dbr = wfGetDB( DB_SLAVE ); 04549 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', 04550 'wl_notificationtimestamp', 04551 array( 04552 'wl_user' => $user->getId(), 04553 'wl_namespace' => $this->getNamespace(), 04554 'wl_title' => $this->getDBkey(), 04555 ), 04556 __METHOD__ 04557 ); 04558 return $this->mNotificationTimestamp[$uid]; 04559 } 04560 04567 public function getNamespaceKey( $prepend = 'nstab-' ) { 04568 global $wgContLang; 04569 // Gets the subject namespace if this title 04570 $namespace = MWNamespace::getSubject( $this->getNamespace() ); 04571 // Checks if canonical namespace name exists for namespace 04572 if ( MWNamespace::exists( $this->getNamespace() ) ) { 04573 // Uses canonical namespace name 04574 $namespaceKey = MWNamespace::getCanonicalName( $namespace ); 04575 } else { 04576 // Uses text of namespace 04577 $namespaceKey = $this->getSubjectNsText(); 04578 } 04579 // Makes namespace key lowercase 04580 $namespaceKey = $wgContLang->lc( $namespaceKey ); 04581 // Uses main 04582 if ( $namespaceKey == '' ) { 04583 $namespaceKey = 'main'; 04584 } 04585 // Changes file to image for backwards compatibility 04586 if ( $namespaceKey == 'file' ) { 04587 $namespaceKey = 'image'; 04588 } 04589 return $prepend . $namespaceKey; 04590 } 04591 04598 public function getRedirectsHere( $ns = null ) { 04599 $redirs = array(); 04600 04601 $dbr = wfGetDB( DB_SLAVE ); 04602 $where = array( 04603 'rd_namespace' => $this->getNamespace(), 04604 'rd_title' => $this->getDBkey(), 04605 'rd_from = page_id' 04606 ); 04607 if ( $this->isExternal() ) { 04608 $where['rd_interwiki'] = $this->getInterwiki(); 04609 } else { 04610 $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'; 04611 } 04612 if ( !is_null( $ns ) ) { 04613 $where['page_namespace'] = $ns; 04614 } 04615 04616 $res = $dbr->select( 04617 array( 'redirect', 'page' ), 04618 array( 'page_namespace', 'page_title' ), 04619 $where, 04620 __METHOD__ 04621 ); 04622 04623 foreach ( $res as $row ) { 04624 $redirs[] = self::newFromRow( $row ); 04625 } 04626 return $redirs; 04627 } 04628 04634 public function isValidRedirectTarget() { 04635 global $wgInvalidRedirectTargets; 04636 04637 // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here 04638 if ( $this->isSpecial( 'Userlogout' ) ) { 04639 return false; 04640 } 04641 04642 foreach ( $wgInvalidRedirectTargets as $target ) { 04643 if ( $this->isSpecial( $target ) ) { 04644 return false; 04645 } 04646 } 04647 04648 return true; 04649 } 04650 04656 public function getBacklinkCache() { 04657 return BacklinkCache::get( $this ); 04658 } 04659 04665 public function canUseNoindex() { 04666 global $wgContentNamespaces, $wgExemptFromUserRobotsControl; 04667 04668 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl ) 04669 ? $wgContentNamespaces 04670 : $wgExemptFromUserRobotsControl; 04671 04672 return !in_array( $this->mNamespace, $bannedNamespaces ); 04673 04674 } 04675 04686 public function getCategorySortkey( $prefix = '' ) { 04687 $unprefixed = $this->getText(); 04688 04689 // Anything that uses this hook should only depend 04690 // on the Title object passed in, and should probably 04691 // tell the users to run updateCollations.php --force 04692 // in order to re-sort existing category relations. 04693 wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) ); 04694 if ( $prefix !== '' ) { 04695 # Separate with a line feed, so the unprefixed part is only used as 04696 # a tiebreaker when two pages have the exact same prefix. 04697 # In UCA, tab is the only character that can sort above LF 04698 # so we strip both of them from the original prefix. 04699 $prefix = strtr( $prefix, "\n\t", ' ' ); 04700 return "$prefix\n$unprefixed"; 04701 } 04702 return $unprefixed; 04703 } 04704 04713 public function getPageLanguage() { 04714 global $wgLang, $wgLanguageCode; 04715 wfProfileIn( __METHOD__ ); 04716 if ( $this->isSpecialPage() ) { 04717 // special pages are in the user language 04718 wfProfileOut( __METHOD__ ); 04719 return $wgLang; 04720 } 04721 04722 // Checking if DB language is set 04723 if ( $this->mDbPageLanguage ) { 04724 wfProfileOut( __METHOD__ ); 04725 return wfGetLangObj( $this->mDbPageLanguage ); 04726 } 04727 04728 if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) { 04729 // Note that this may depend on user settings, so the cache should 04730 // be only per-request. 04731 // NOTE: ContentHandler::getPageLanguage() may need to load the 04732 // content to determine the page language! 04733 // Checking $wgLanguageCode hasn't changed for the benefit of unit 04734 // tests. 04735 $contentHandler = ContentHandler::getForTitle( $this ); 04736 $langObj = wfGetLangObj( $contentHandler->getPageLanguage( $this ) ); 04737 $this->mPageLanguage = array( $langObj->getCode(), $wgLanguageCode ); 04738 } else { 04739 $langObj = wfGetLangObj( $this->mPageLanguage[0] ); 04740 } 04741 04742 wfProfileOut( __METHOD__ ); 04743 return $langObj; 04744 } 04745 04754 public function getPageViewLanguage() { 04755 global $wgLang; 04756 04757 if ( $this->isSpecialPage() ) { 04758 // If the user chooses a variant, the content is actually 04759 // in a language whose code is the variant code. 04760 $variant = $wgLang->getPreferredVariant(); 04761 if ( $wgLang->getCode() !== $variant ) { 04762 return Language::factory( $variant ); 04763 } 04764 04765 return $wgLang; 04766 } 04767 04768 // @note Can't be cached persistently, depends on user settings. 04769 // @note ContentHandler::getPageViewLanguage() may need to load the 04770 // content to determine the page language! 04771 $contentHandler = ContentHandler::getForTitle( $this ); 04772 $pageLang = $contentHandler->getPageViewLanguage( $this ); 04773 return $pageLang; 04774 } 04775 04786 public function getEditNotices( $oldid = 0 ) { 04787 $notices = array(); 04788 04789 # Optional notices on a per-namespace and per-page basis 04790 $editnotice_ns = 'editnotice-' . $this->getNamespace(); 04791 $editnotice_ns_message = wfMessage( $editnotice_ns ); 04792 if ( $editnotice_ns_message->exists() ) { 04793 $notices[$editnotice_ns] = $editnotice_ns_message->parseAsBlock(); 04794 } 04795 if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) { 04796 $parts = explode( '/', $this->getDBkey() ); 04797 $editnotice_base = $editnotice_ns; 04798 while ( count( $parts ) > 0 ) { 04799 $editnotice_base .= '-' . array_shift( $parts ); 04800 $editnotice_base_msg = wfMessage( $editnotice_base ); 04801 if ( $editnotice_base_msg->exists() ) { 04802 $notices[$editnotice_base] = $editnotice_base_msg->parseAsBlock(); 04803 } 04804 } 04805 } else { 04806 # Even if there are no subpages in namespace, we still don't want / in MW ns. 04807 $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() ); 04808 $editnoticeMsg = wfMessage( $editnoticeText ); 04809 if ( $editnoticeMsg->exists() ) { 04810 $notices[$editnoticeText] = $editnoticeMsg->parseAsBlock(); 04811 } 04812 } 04813 04814 wfRunHooks( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) ); 04815 return $notices; 04816 } 04817 }