MediaWiki
REL1_19
|
00001 <?php 00031 class Title { 00033 // @{ 00034 static private $titleCache = array(); 00035 // @} 00036 00042 const CACHE_MAX = 1000; 00043 00048 const GAID_FOR_UPDATE = 1; 00049 00055 // @{ 00056 00057 var $mTextform = ''; // /< Text form (spaces not underscores) of the main part 00058 var $mUrlform = ''; // /< URL-encoded form of the main part 00059 var $mDbkeyform = ''; // /< Main part with underscores 00060 var $mUserCaseDBKey; // /< DB key with the initial letter in the case specified by the user 00061 var $mNamespace = NS_MAIN; // /< Namespace index, i.e. one of the NS_xxxx constants 00062 var $mInterwiki = ''; // /< Interwiki prefix (or null string) 00063 var $mFragment; // /< Title fragment (i.e. the bit after the #) 00064 var $mArticleID = -1; // /< Article ID, fetched from the link cache on demand 00065 var $mLatestID = false; // /< ID of most recent revision 00066 private $mEstimateRevisions; // /< Estimated number of revisions; null of not loaded 00067 var $mRestrictions = array(); // /< Array of groups allowed to edit this article 00068 var $mOldRestrictions = false; 00069 var $mCascadeRestriction; 00070 var $mCascadingRestrictions; // Caching the results of getCascadeProtectionSources 00071 var $mRestrictionsExpiry = array(); 00072 var $mHasCascadingRestrictions; 00073 var $mCascadeSources; 00074 var $mRestrictionsLoaded = false; 00075 var $mPrefixedText; 00076 var $mTitleProtection; 00077 # Don't change the following default, NS_MAIN is hardcoded in several 00078 # places. See bug 696. 00079 var $mDefaultNamespace = NS_MAIN; // /< Namespace index when there is no namespace 00080 # Zero except in {{transclusion}} tags 00081 var $mWatched = null; // /< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching() 00082 var $mLength = -1; // /< The page length, 0 for special pages 00083 var $mRedirect = null; // /< Is the article at this title a redirect? 00084 var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false 00085 var $mBacklinkCache = null; // /< Cache of links to this title 00086 // @} 00087 00088 00092 /*protected*/ function __construct() { } 00093 00102 public static function newFromDBkey( $key ) { 00103 $t = new Title(); 00104 $t->mDbkeyform = $key; 00105 if ( $t->secureAndSplit() ) { 00106 return $t; 00107 } else { 00108 return null; 00109 } 00110 } 00111 00124 public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { 00125 if ( is_object( $text ) ) { 00126 throw new MWException( 'Title::newFromText given an object' ); 00127 } 00128 00137 if ( $defaultNamespace == NS_MAIN && isset( Title::$titleCache[$text] ) ) { 00138 return Title::$titleCache[$text]; 00139 } 00140 00141 # Convert things like é ā or 〗 into normalized (bug 14952) text 00142 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); 00143 00144 $t = new Title(); 00145 $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); 00146 $t->mDefaultNamespace = $defaultNamespace; 00147 00148 static $cachedcount = 0 ; 00149 if ( $t->secureAndSplit() ) { 00150 if ( $defaultNamespace == NS_MAIN ) { 00151 if ( $cachedcount >= self::CACHE_MAX ) { 00152 # Avoid memory leaks on mass operations... 00153 Title::$titleCache = array(); 00154 $cachedcount = 0; 00155 } 00156 $cachedcount++; 00157 Title::$titleCache[$text] =& $t; 00158 } 00159 return $t; 00160 } else { 00161 $ret = null; 00162 return $ret; 00163 } 00164 } 00165 00181 public static function newFromURL( $url ) { 00182 global $wgLegalTitleChars; 00183 $t = new Title(); 00184 00185 # For compatibility with old buggy URLs. "+" is usually not valid in titles, 00186 # but some URLs used it as a space replacement and they still come 00187 # from some external search tools. 00188 if ( strpos( $wgLegalTitleChars, '+' ) === false ) { 00189 $url = str_replace( '+', ' ', $url ); 00190 } 00191 00192 $t->mDbkeyform = str_replace( ' ', '_', $url ); 00193 if ( $t->secureAndSplit() ) { 00194 return $t; 00195 } else { 00196 return null; 00197 } 00198 } 00199 00207 public static function newFromID( $id, $flags = 0 ) { 00208 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 00209 $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ ); 00210 if ( $row !== false ) { 00211 $title = Title::newFromRow( $row ); 00212 } else { 00213 $title = null; 00214 } 00215 return $title; 00216 } 00217 00224 public static function newFromIDs( $ids ) { 00225 if ( !count( $ids ) ) { 00226 return array(); 00227 } 00228 $dbr = wfGetDB( DB_SLAVE ); 00229 00230 $res = $dbr->select( 00231 'page', 00232 array( 00233 'page_namespace', 'page_title', 'page_id', 00234 'page_len', 'page_is_redirect', 'page_latest', 00235 ), 00236 array( 'page_id' => $ids ), 00237 __METHOD__ 00238 ); 00239 00240 $titles = array(); 00241 foreach ( $res as $row ) { 00242 $titles[] = Title::newFromRow( $row ); 00243 } 00244 return $titles; 00245 } 00246 00253 public static function newFromRow( $row ) { 00254 $t = self::makeTitle( $row->page_namespace, $row->page_title ); 00255 $t->loadFromRow( $row ); 00256 return $t; 00257 } 00258 00266 public function loadFromRow( $row ) { 00267 if ( $row ) { // page found 00268 if ( isset( $row->page_id ) ) 00269 $this->mArticleID = (int)$row->page_id; 00270 if ( isset( $row->page_len ) ) 00271 $this->mLength = (int)$row->page_len; 00272 if ( isset( $row->page_is_redirect ) ) 00273 $this->mRedirect = (bool)$row->page_is_redirect; 00274 if ( isset( $row->page_latest ) ) 00275 $this->mLatestID = (int)$row->page_latest; 00276 } else { // page not found 00277 $this->mArticleID = 0; 00278 $this->mLength = 0; 00279 $this->mRedirect = false; 00280 $this->mLatestID = 0; 00281 } 00282 } 00283 00297 public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { 00298 $t = new Title(); 00299 $t->mInterwiki = $interwiki; 00300 $t->mFragment = $fragment; 00301 $t->mNamespace = $ns = intval( $ns ); 00302 $t->mDbkeyform = str_replace( ' ', '_', $title ); 00303 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; 00304 $t->mUrlform = wfUrlencode( $t->mDbkeyform ); 00305 $t->mTextform = str_replace( '_', ' ', $title ); 00306 return $t; 00307 } 00308 00320 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { 00321 $t = new Title(); 00322 $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki ); 00323 if ( $t->secureAndSplit() ) { 00324 return $t; 00325 } else { 00326 return null; 00327 } 00328 } 00329 00335 public static function newMainPage() { 00336 $title = Title::newFromText( wfMsgForContent( 'mainpage' ) ); 00337 // Don't give fatal errors if the message is broken 00338 if ( !$title ) { 00339 $title = Title::newFromText( 'Main Page' ); 00340 } 00341 return $title; 00342 } 00343 00353 public static function newFromRedirect( $text ) { 00354 return self::newFromRedirectInternal( $text ); 00355 } 00356 00366 public static function newFromRedirectRecurse( $text ) { 00367 $titles = self::newFromRedirectArray( $text ); 00368 return $titles ? array_pop( $titles ) : null; 00369 } 00370 00380 public static function newFromRedirectArray( $text ) { 00381 global $wgMaxRedirects; 00382 $title = self::newFromRedirectInternal( $text ); 00383 if ( is_null( $title ) ) { 00384 return null; 00385 } 00386 // recursive check to follow double redirects 00387 $recurse = $wgMaxRedirects; 00388 $titles = array( $title ); 00389 while ( --$recurse > 0 ) { 00390 if ( $title->isRedirect() ) { 00391 $page = WikiPage::factory( $title ); 00392 $newtitle = $page->getRedirectTarget(); 00393 } else { 00394 break; 00395 } 00396 // Redirects to some special pages are not permitted 00397 if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) { 00398 // the new title passes the checks, so make that our current title so that further recursion can be checked 00399 $title = $newtitle; 00400 $titles[] = $newtitle; 00401 } else { 00402 break; 00403 } 00404 } 00405 return $titles; 00406 } 00407 00415 protected static function newFromRedirectInternal( $text ) { 00416 global $wgMaxRedirects; 00417 if ( $wgMaxRedirects < 1 ) { 00418 //redirects are disabled, so quit early 00419 return null; 00420 } 00421 $redir = MagicWord::get( 'redirect' ); 00422 $text = trim( $text ); 00423 if ( $redir->matchStartAndRemove( $text ) ) { 00424 // Extract the first link and see if it's usable 00425 // Ensure that it really does come directly after #REDIRECT 00426 // Some older redirects included a colon, so don't freak about that! 00427 $m = array(); 00428 if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) { 00429 // Strip preceding colon used to "escape" categories, etc. 00430 // and URL-decode links 00431 if ( strpos( $m[1], '%' ) !== false ) { 00432 // Match behavior of inline link parsing here; 00433 $m[1] = rawurldecode( ltrim( $m[1], ':' ) ); 00434 } 00435 $title = Title::newFromText( $m[1] ); 00436 // If the title is a redirect to bad special pages or is invalid, return null 00437 if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) { 00438 return null; 00439 } 00440 return $title; 00441 } 00442 } 00443 return null; 00444 } 00445 00452 public static function nameOf( $id ) { 00453 $dbr = wfGetDB( DB_SLAVE ); 00454 00455 $s = $dbr->selectRow( 00456 'page', 00457 array( 'page_namespace', 'page_title' ), 00458 array( 'page_id' => $id ), 00459 __METHOD__ 00460 ); 00461 if ( $s === false ) { 00462 return null; 00463 } 00464 00465 $n = self::makeName( $s->page_namespace, $s->page_title ); 00466 return $n; 00467 } 00468 00474 public static function legalChars() { 00475 global $wgLegalTitleChars; 00476 return $wgLegalTitleChars; 00477 } 00478 00486 static function getTitleInvalidRegex() { 00487 static $rxTc = false; 00488 if ( !$rxTc ) { 00489 # Matching titles will be held as illegal. 00490 $rxTc = '/' . 00491 # Any character not allowed is forbidden... 00492 '[^' . self::legalChars() . ']' . 00493 # URL percent encoding sequences interfere with the ability 00494 # to round-trip titles -- you can't link to them consistently. 00495 '|%[0-9A-Fa-f]{2}' . 00496 # XML/HTML character references produce similar issues. 00497 '|&[A-Za-z0-9\x80-\xff]+;' . 00498 '|&#[0-9]+;' . 00499 '|&#x[0-9A-Fa-f]+;' . 00500 '/S'; 00501 } 00502 00503 return $rxTc; 00504 } 00505 00514 public static function indexTitle( $ns, $title ) { 00515 global $wgContLang; 00516 00517 $lc = SearchEngine::legalSearchChars() . '&#;'; 00518 $t = $wgContLang->normalizeForSearch( $title ); 00519 $t = preg_replace( "/[^{$lc}]+/", ' ', $t ); 00520 $t = $wgContLang->lc( $t ); 00521 00522 # Handle 's, s' 00523 $t = preg_replace( "/([{$lc}]+)'s( |$)/", "\\1 \\1's ", $t ); 00524 $t = preg_replace( "/([{$lc}]+)s'( |$)/", "\\1s ", $t ); 00525 00526 $t = preg_replace( "/\\s+/", ' ', $t ); 00527 00528 if ( $ns == NS_FILE ) { 00529 $t = preg_replace( "/ (png|gif|jpg|jpeg|ogg)$/", "", $t ); 00530 } 00531 return trim( $t ); 00532 } 00533 00543 public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) { 00544 global $wgContLang; 00545 00546 $namespace = $wgContLang->getNsText( $ns ); 00547 $name = $namespace == '' ? $title : "$namespace:$title"; 00548 if ( strval( $interwiki ) != '' ) { 00549 $name = "$interwiki:$name"; 00550 } 00551 if ( strval( $fragment ) != '' ) { 00552 $name .= '#' . $fragment; 00553 } 00554 return $name; 00555 } 00556 00563 static function escapeFragmentForURL( $fragment ) { 00564 # Note that we don't urlencode the fragment. urlencoded Unicode 00565 # fragments appear not to work in IE (at least up to 7) or in at least 00566 # one version of Opera 9.x. The W3C validator, for one, doesn't seem 00567 # to care if they aren't encoded. 00568 return Sanitizer::escapeId( $fragment, 'noninitial' ); 00569 } 00570 00579 public static function compare( $a, $b ) { 00580 if ( $a->getNamespace() == $b->getNamespace() ) { 00581 return strcmp( $a->getText(), $b->getText() ); 00582 } else { 00583 return $a->getNamespace() - $b->getNamespace(); 00584 } 00585 } 00586 00593 public function isLocal() { 00594 if ( $this->mInterwiki != '' ) { 00595 return Interwiki::fetch( $this->mInterwiki )->isLocal(); 00596 } else { 00597 return true; 00598 } 00599 } 00600 00606 public function isExternal() { 00607 return ( $this->mInterwiki != '' ); 00608 } 00609 00615 public function getInterwiki() { 00616 return $this->mInterwiki; 00617 } 00618 00625 public function isTrans() { 00626 if ( $this->mInterwiki == '' ) { 00627 return false; 00628 } 00629 00630 return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); 00631 } 00632 00638 public function getTransWikiID() { 00639 if ( $this->mInterwiki == '' ) { 00640 return false; 00641 } 00642 00643 return Interwiki::fetch( $this->mInterwiki )->getWikiID(); 00644 } 00645 00651 public function getText() { 00652 return $this->mTextform; 00653 } 00654 00660 public function getPartialURL() { 00661 return $this->mUrlform; 00662 } 00663 00669 public function getDBkey() { 00670 return $this->mDbkeyform; 00671 } 00672 00678 function getUserCaseDBKey() { 00679 return $this->mUserCaseDBKey; 00680 } 00681 00687 public function getNamespace() { 00688 return $this->mNamespace; 00689 } 00690 00696 public function getNsText() { 00697 global $wgContLang; 00698 00699 if ( $this->mInterwiki != '' ) { 00700 // This probably shouldn't even happen. ohh man, oh yuck. 00701 // But for interwiki transclusion it sometimes does. 00702 // Shit. Shit shit shit. 00703 // 00704 // Use the canonical namespaces if possible to try to 00705 // resolve a foreign namespace. 00706 if ( MWNamespace::exists( $this->mNamespace ) ) { 00707 return MWNamespace::getCanonicalName( $this->mNamespace ); 00708 } 00709 } 00710 00711 // Strip off subpages 00712 $pagename = $this->getText(); 00713 if ( strpos( $pagename, '/' ) !== false ) { 00714 list( $username , ) = explode( '/', $pagename, 2 ); 00715 } else { 00716 $username = $pagename; 00717 } 00718 00719 if ( $wgContLang->needsGenderDistinction() && 00720 MWNamespace::hasGenderDistinction( $this->mNamespace ) ) { 00721 $gender = GenderCache::singleton()->getGenderOf( $username, __METHOD__ ); 00722 return $wgContLang->getGenderNsText( $this->mNamespace, $gender ); 00723 } 00724 00725 return $wgContLang->getNsText( $this->mNamespace ); 00726 } 00727 00733 public function getSubjectNsText() { 00734 global $wgContLang; 00735 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) ); 00736 } 00737 00743 public function getTalkNsText() { 00744 global $wgContLang; 00745 return( $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) ) ); 00746 } 00747 00753 public function canTalk() { 00754 return( MWNamespace::canTalk( $this->mNamespace ) ); 00755 } 00756 00763 public function canExist() { 00764 return $this->mNamespace >= NS_MAIN; 00765 } 00766 00772 public function isWatchable() { 00773 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); 00774 } 00775 00781 public function isSpecialPage() { 00782 return $this->getNamespace() == NS_SPECIAL; 00783 } 00784 00791 public function isSpecial( $name ) { 00792 if ( $this->isSpecialPage() ) { 00793 list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); 00794 if ( $name == $thisName ) { 00795 return true; 00796 } 00797 } 00798 return false; 00799 } 00800 00807 public function fixSpecialName() { 00808 if ( $this->isSpecialPage() ) { 00809 list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); 00810 if ( $canonicalName ) { 00811 $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); 00812 if ( $localName != $this->mDbkeyform ) { 00813 return Title::makeTitle( NS_SPECIAL, $localName ); 00814 } 00815 } 00816 } 00817 return $this; 00818 } 00819 00830 public function inNamespace( $ns ) { 00831 return MWNamespace::equals( $this->getNamespace(), $ns ); 00832 } 00833 00841 public function inNamespaces( /* ... */ ) { 00842 $namespaces = func_get_args(); 00843 if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { 00844 $namespaces = $namespaces[0]; 00845 } 00846 00847 foreach ( $namespaces as $ns ) { 00848 if ( $this->inNamespace( $ns ) ) { 00849 return true; 00850 } 00851 } 00852 00853 return false; 00854 } 00855 00867 public function hasSubjectNamespace( $ns ) { 00868 return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); 00869 } 00870 00878 public function isContentPage() { 00879 return MWNamespace::isContent( $this->getNamespace() ); 00880 } 00881 00888 public function isMovable() { 00889 if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) { 00890 // Interwiki title or immovable namespace. Hooks don't get to override here 00891 return false; 00892 } 00893 00894 $result = true; 00895 wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); 00896 return $result; 00897 } 00898 00909 public function isMainPage() { 00910 return $this->equals( Title::newMainPage() ); 00911 } 00912 00918 public function isSubpage() { 00919 return MWNamespace::hasSubpages( $this->mNamespace ) 00920 ? strpos( $this->getText(), '/' ) !== false 00921 : false; 00922 } 00923 00929 public function isConversionTable() { 00930 return $this->getNamespace() == NS_MEDIAWIKI && 00931 strpos( $this->getText(), 'Conversiontable' ) !== false; 00932 } 00933 00939 public function isWikitextPage() { 00940 $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage(); 00941 wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) ); 00942 return $retval; 00943 } 00944 00951 public function isCssOrJsPage() { 00952 $retval = $this->mNamespace == NS_MEDIAWIKI 00953 && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0; 00954 wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) ); 00955 return $retval; 00956 } 00957 00962 public function isCssJsSubpage() { 00963 return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); 00964 } 00965 00971 public function getSkinFromCssJsSubpage() { 00972 $subpage = explode( '/', $this->mTextform ); 00973 $subpage = $subpage[ count( $subpage ) - 1 ]; 00974 $lastdot = strrpos( $subpage, '.' ); 00975 if ( $lastdot === false ) 00976 return $subpage; # Never happens: only called for names ending in '.css' or '.js' 00977 return substr( $subpage, 0, $lastdot ); 00978 } 00979 00985 public function isCssSubpage() { 00986 return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) ); 00987 } 00988 00994 public function isJsSubpage() { 00995 return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) ); 00996 } 00997 01003 public function isTalkPage() { 01004 return MWNamespace::isTalk( $this->getNamespace() ); 01005 } 01006 01012 public function getTalkPage() { 01013 return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); 01014 } 01015 01022 public function getSubjectPage() { 01023 // Is this the same title? 01024 $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); 01025 if ( $this->getNamespace() == $subjectNS ) { 01026 return $this; 01027 } 01028 return Title::makeTitle( $subjectNS, $this->getDBkey() ); 01029 } 01030 01036 public function getDefaultNamespace() { 01037 return $this->mDefaultNamespace; 01038 } 01039 01046 public function getIndexTitle() { 01047 return Title::indexTitle( $this->mNamespace, $this->mTextform ); 01048 } 01049 01055 public function getFragment() { 01056 return $this->mFragment; 01057 } 01058 01063 public function getFragmentForURL() { 01064 if ( $this->mFragment == '' ) { 01065 return ''; 01066 } else { 01067 return '#' . Title::escapeFragmentForURL( $this->mFragment ); 01068 } 01069 } 01070 01081 public function setFragment( $fragment ) { 01082 $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); 01083 } 01084 01093 private function prefix( $name ) { 01094 $p = ''; 01095 if ( $this->mInterwiki != '' ) { 01096 $p = $this->mInterwiki . ':'; 01097 } 01098 01099 if ( 0 != $this->mNamespace ) { 01100 $p .= $this->getNsText() . ':'; 01101 } 01102 return $p . $name; 01103 } 01104 01111 public function getPrefixedDBkey() { 01112 $s = $this->prefix( $this->mDbkeyform ); 01113 $s = str_replace( ' ', '_', $s ); 01114 return $s; 01115 } 01116 01123 public function getPrefixedText() { 01124 // @todo FIXME: Bad usage of empty() ? 01125 if ( empty( $this->mPrefixedText ) ) { 01126 $s = $this->prefix( $this->mTextform ); 01127 $s = str_replace( '_', ' ', $s ); 01128 $this->mPrefixedText = $s; 01129 } 01130 return $this->mPrefixedText; 01131 } 01132 01138 public function __toString() { 01139 return $this->getPrefixedText(); 01140 } 01141 01148 public function getFullText() { 01149 $text = $this->getPrefixedText(); 01150 if ( $this->mFragment != '' ) { 01151 $text .= '#' . $this->mFragment; 01152 } 01153 return $text; 01154 } 01155 01161 public function getBaseText() { 01162 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01163 return $this->getText(); 01164 } 01165 01166 $parts = explode( '/', $this->getText() ); 01167 # Don't discard the real title if there's no subpage involved 01168 if ( count( $parts ) > 1 ) { 01169 unset( $parts[count( $parts ) - 1] ); 01170 } 01171 return implode( '/', $parts ); 01172 } 01173 01179 public function getSubpageText() { 01180 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 01181 return( $this->mTextform ); 01182 } 01183 $parts = explode( '/', $this->mTextform ); 01184 return( $parts[count( $parts ) - 1] ); 01185 } 01186 01193 public function getEscapedText() { 01194 wfDeprecated( __METHOD__, '1.19' ); 01195 return htmlspecialchars( $this->getPrefixedText() ); 01196 } 01197 01203 public function getSubpageUrlForm() { 01204 $text = $this->getSubpageText(); 01205 $text = wfUrlencode( str_replace( ' ', '_', $text ) ); 01206 return( $text ); 01207 } 01208 01214 public function getPrefixedURL() { 01215 $s = $this->prefix( $this->mDbkeyform ); 01216 $s = wfUrlencode( str_replace( ' ', '_', $s ) ); 01217 return $s; 01218 } 01219 01230 private static function fixUrlQueryArgs( $query, $query2 = false ) { 01231 if( $query2 !== false ) { 01232 wfDeprecated( "Title::get{Canonical,Full,Link,Local} method called with a second parameter is deprecated. Add your parameter to an array passed as the first parameter.", "1.19" ); 01233 } 01234 if ( is_array( $query ) ) { 01235 $query = wfArrayToCGI( $query ); 01236 } 01237 if ( $query2 ) { 01238 if ( is_string( $query2 ) ) { 01239 // $query2 is a string, we will consider this to be 01240 // a deprecated $variant argument and add it to the query 01241 $query2 = wfArrayToCGI( array( 'variant' => $query2 ) ); 01242 } else { 01243 $query2 = wfArrayToCGI( $query2 ); 01244 } 01245 // If we have $query content add a & to it first 01246 if ( $query ) { 01247 $query .= '&'; 01248 } 01249 // Now append the queries together 01250 $query .= $query2; 01251 } 01252 return $query; 01253 } 01254 01264 public function getFullURL( $query = '', $query2 = false ) { 01265 $query = self::fixUrlQueryArgs( $query, $query2 ); 01266 01267 # Hand off all the decisions on urls to getLocalURL 01268 $url = $this->getLocalURL( $query ); 01269 01270 # Expand the url to make it a full url. Note that getLocalURL has the 01271 # potential to output full urls for a variety of reasons, so we use 01272 # wfExpandUrl instead of simply prepending $wgServer 01273 $url = wfExpandUrl( $url, PROTO_RELATIVE ); 01274 01275 # Finally, add the fragment. 01276 $url .= $this->getFragmentForURL(); 01277 01278 wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) ); 01279 return $url; 01280 } 01281 01300 public function getLocalURL( $query = '', $query2 = false ) { 01301 global $wgArticlePath, $wgScript, $wgServer, $wgRequest; 01302 01303 $query = self::fixUrlQueryArgs( $query, $query2 ); 01304 01305 $interwiki = Interwiki::fetch( $this->mInterwiki ); 01306 if ( $interwiki ) { 01307 $namespace = $this->getNsText(); 01308 if ( $namespace != '' ) { 01309 # Can this actually happen? Interwikis shouldn't be parsed. 01310 # Yes! It can in interwiki transclusion. But... it probably shouldn't. 01311 $namespace .= ':'; 01312 } 01313 $url = $interwiki->getURL( $namespace . $this->getDBkey() ); 01314 $url = wfAppendQuery( $url, $query ); 01315 } else { 01316 $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); 01317 if ( $query == '' ) { 01318 $url = str_replace( '$1', $dbkey, $wgArticlePath ); 01319 wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) ); 01320 } else { 01321 global $wgVariantArticlePath, $wgActionPaths; 01322 $url = false; 01323 $matches = array(); 01324 01325 if ( !empty( $wgActionPaths ) && 01326 preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) ) 01327 { 01328 $action = urldecode( $matches[2] ); 01329 if ( isset( $wgActionPaths[$action] ) ) { 01330 $query = $matches[1]; 01331 if ( isset( $matches[4] ) ) { 01332 $query .= $matches[4]; 01333 } 01334 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] ); 01335 if ( $query != '' ) { 01336 $url = wfAppendQuery( $url, $query ); 01337 } 01338 } 01339 } 01340 01341 if ( $url === false && 01342 $wgVariantArticlePath && 01343 $this->getPageLanguage()->hasVariants() && 01344 preg_match( '/^variant=([^&]*)$/', $query, $matches ) ) 01345 { 01346 $variant = urldecode( $matches[1] ); 01347 if ( $this->getPageLanguage()->hasVariant( $variant ) ) { 01348 // Only do the variant replacement if the given variant is a valid 01349 // variant for the page's language. 01350 $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); 01351 $url = str_replace( '$1', $dbkey, $url ); 01352 } 01353 } 01354 01355 if ( $url === false ) { 01356 if ( $query == '-' ) { 01357 $query = ''; 01358 } 01359 $url = "{$wgScript}?title={$dbkey}&{$query}"; 01360 } 01361 } 01362 01363 wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) ); 01364 01365 // @todo FIXME: This causes breakage in various places when we 01366 // actually expected a local URL and end up with dupe prefixes. 01367 if ( $wgRequest->getVal( 'action' ) == 'render' ) { 01368 $url = $wgServer . $url; 01369 } 01370 } 01371 wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) ); 01372 return $url; 01373 } 01374 01390 public function getLinkURL( $query = '', $query2 = false ) { 01391 wfProfileIn( __METHOD__ ); 01392 if ( $this->isExternal() ) { 01393 $ret = $this->getFullURL( $query, $query2 ); 01394 } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { 01395 $ret = $this->getFragmentForURL(); 01396 } else { 01397 $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL(); 01398 } 01399 wfProfileOut( __METHOD__ ); 01400 return $ret; 01401 } 01402 01412 public function escapeLocalURL( $query = '', $query2 = false ) { 01413 wfDeprecated( __METHOD__, '1.19' ); 01414 return htmlspecialchars( $this->getLocalURL( $query, $query2 ) ); 01415 } 01416 01426 public function escapeFullURL( $query = '', $query2 = false ) { 01427 wfDeprecated( __METHOD__, '1.19' ); 01428 return htmlspecialchars( $this->getFullURL( $query, $query2 ) ); 01429 } 01430 01445 public function getInternalURL( $query = '', $query2 = false ) { 01446 global $wgInternalServer, $wgServer; 01447 $query = self::fixUrlQueryArgs( $query, $query2 ); 01448 $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; 01449 $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP ); 01450 wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) ); 01451 return $url; 01452 } 01453 01467 public function getCanonicalURL( $query = '', $query2 = false ) { 01468 $query = self::fixUrlQueryArgs( $query, $query2 ); 01469 $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL ); 01470 wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) ); 01471 return $url; 01472 } 01473 01482 public function escapeCanonicalURL( $query = '', $query2 = false ) { 01483 wfDeprecated( __METHOD__, '1.19' ); 01484 return htmlspecialchars( $this->getCanonicalURL( $query, $query2 ) ); 01485 } 01486 01493 public function getEditURL() { 01494 if ( $this->mInterwiki != '' ) { 01495 return ''; 01496 } 01497 $s = $this->getLocalURL( 'action=edit' ); 01498 01499 return $s; 01500 } 01501 01507 public function userIsWatching() { 01508 global $wgUser; 01509 01510 if ( is_null( $this->mWatched ) ) { 01511 if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) { 01512 $this->mWatched = false; 01513 } else { 01514 $this->mWatched = $wgUser->isWatched( $this ); 01515 } 01516 } 01517 return $this->mWatched; 01518 } 01519 01527 public function userCanRead() { 01528 wfDeprecated( __METHOD__, '1.19' ); 01529 return $this->userCan( 'read' ); 01530 } 01531 01547 public function quickUserCan( $action, $user = null ) { 01548 return $this->userCan( $action, $user, false ); 01549 } 01550 01561 public function userCan( $action, $user = null, $doExpensiveQueries = true ) { 01562 if ( !$user instanceof User ) { 01563 global $wgUser; 01564 $user = $wgUser; 01565 } 01566 return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries, true ) ); 01567 } 01568 01582 public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { 01583 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); 01584 01585 // Remove the errors being ignored. 01586 foreach ( $errors as $index => $error ) { 01587 $error_key = is_array( $error ) ? $error[0] : $error; 01588 01589 if ( in_array( $error_key, $ignoreErrors ) ) { 01590 unset( $errors[$index] ); 01591 } 01592 } 01593 01594 return $errors; 01595 } 01596 01608 private function checkQuickPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01609 if ( $action == 'create' ) { 01610 if ( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || 01611 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) { 01612 $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' ); 01613 } 01614 } elseif ( $action == 'move' ) { 01615 if ( !$user->isAllowed( 'move-rootuserpages' ) 01616 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01617 // Show user page-specific message only if the user can move other pages 01618 $errors[] = array( 'cant-move-user-page' ); 01619 } 01620 01621 // Check if user is allowed to move files if it's a file 01622 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) { 01623 $errors[] = array( 'movenotallowedfile' ); 01624 } 01625 01626 if ( !$user->isAllowed( 'move' ) ) { 01627 // User can't move anything 01628 global $wgGroupPermissions; 01629 $userCanMove = false; 01630 if ( isset( $wgGroupPermissions['user']['move'] ) ) { 01631 $userCanMove = $wgGroupPermissions['user']['move']; 01632 } 01633 $autoconfirmedCanMove = false; 01634 if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) { 01635 $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move']; 01636 } 01637 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { 01638 // custom message if logged-in users without any special rights can move 01639 $errors[] = array( 'movenologintext' ); 01640 } else { 01641 $errors[] = array( 'movenotallowed' ); 01642 } 01643 } 01644 } elseif ( $action == 'move-target' ) { 01645 if ( !$user->isAllowed( 'move' ) ) { 01646 // User can't move anything 01647 $errors[] = array( 'movenotallowed' ); 01648 } elseif ( !$user->isAllowed( 'move-rootuserpages' ) 01649 && $this->mNamespace == NS_USER && !$this->isSubpage() ) { 01650 // Show user page-specific message only if the user can move other pages 01651 $errors[] = array( 'cant-move-to-user-page' ); 01652 } 01653 } elseif ( !$user->isAllowed( $action ) ) { 01654 $errors[] = $this->missingPermissionError( $action, $short ); 01655 } 01656 01657 return $errors; 01658 } 01659 01668 private function resultToError( $errors, $result ) { 01669 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) { 01670 // A single array representing an error 01671 $errors[] = $result; 01672 } elseif ( is_array( $result ) && is_array( $result[0] ) ) { 01673 // A nested array representing multiple errors 01674 $errors = array_merge( $errors, $result ); 01675 } elseif ( $result !== '' && is_string( $result ) ) { 01676 // A string representing a message-id 01677 $errors[] = array( $result ); 01678 } elseif ( $result === false ) { 01679 // a generic "We don't want them to do that" 01680 $errors[] = array( 'badaccess-group0' ); 01681 } 01682 return $errors; 01683 } 01684 01696 private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) { 01697 // Use getUserPermissionsErrors instead 01698 $result = ''; 01699 if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { 01700 return $result ? array() : array( array( 'badaccess-group0' ) ); 01701 } 01702 // Check getUserPermissionsErrors hook 01703 if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { 01704 $errors = $this->resultToError( $errors, $result ); 01705 } 01706 // Check getUserPermissionsErrorsExpensive hook 01707 if ( $doExpensiveQueries && !( $short && count( $errors ) > 0 ) && 01708 !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) { 01709 $errors = $this->resultToError( $errors, $result ); 01710 } 01711 01712 return $errors; 01713 } 01714 01726 private function checkSpecialsAndNSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01727 # Only 'createaccount' and 'execute' can be performed on 01728 # special pages, which don't actually exist in the DB. 01729 $specialOKActions = array( 'createaccount', 'execute', 'read' ); 01730 if ( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions ) ) { 01731 $errors[] = array( 'ns-specialprotected' ); 01732 } 01733 01734 # Check $wgNamespaceProtection for restricted namespaces 01735 if ( $this->isNamespaceProtected( $user ) ) { 01736 $ns = $this->mNamespace == NS_MAIN ? 01737 wfMsg( 'nstab-main' ) : $this->getNsText(); 01738 $errors[] = $this->mNamespace == NS_MEDIAWIKI ? 01739 array( 'protectedinterface' ) : array( 'namespaceprotected', $ns ); 01740 } 01741 01742 return $errors; 01743 } 01744 01756 private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01757 # Protect css/js subpages of user pages 01758 # XXX: this might be better using restrictions 01759 # XXX: right 'editusercssjs' is deprecated, for backward compatibility only 01760 if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) 01761 && !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) { 01762 if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) { 01763 $errors[] = array( 'customcssprotected' ); 01764 } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) { 01765 $errors[] = array( 'customjsprotected' ); 01766 } 01767 } 01768 01769 return $errors; 01770 } 01771 01785 private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01786 foreach ( $this->getRestrictions( $action ) as $right ) { 01787 // Backwards compatibility, rewrite sysop -> protect 01788 if ( $right == 'sysop' ) { 01789 $right = 'protect'; 01790 } 01791 if ( $right != '' && !$user->isAllowed( $right ) ) { 01792 // Users with 'editprotected' permission can edit protected pages 01793 // without cascading option turned on. 01794 if ( $action != 'edit' || !$user->isAllowed( 'editprotected' ) 01795 || $this->mCascadeRestriction ) 01796 { 01797 $errors[] = array( 'protectedpagetext', $right ); 01798 } 01799 } 01800 } 01801 01802 return $errors; 01803 } 01804 01816 private function checkCascadingSourcesRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01817 if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { 01818 # We /could/ use the protection level on the source page, but it's 01819 # fairly ugly as we have to establish a precedence hierarchy for pages 01820 # included by multiple cascade-protected pages. So just restrict 01821 # it to people with 'protect' permission, as they could remove the 01822 # protection anyway. 01823 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources(); 01824 # Cascading protection depends on more than this page... 01825 # Several cascading protected pages may include this page... 01826 # Check each cascading level 01827 # This is only for protection restrictions, not for all actions 01828 if ( isset( $restrictions[$action] ) ) { 01829 foreach ( $restrictions[$action] as $right ) { 01830 $right = ( $right == 'sysop' ) ? 'protect' : $right; 01831 if ( $right != '' && !$user->isAllowed( $right ) ) { 01832 $pages = ''; 01833 foreach ( $cascadingSources as $page ) 01834 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; 01835 $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); 01836 } 01837 } 01838 } 01839 } 01840 01841 return $errors; 01842 } 01843 01855 private function checkActionPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01856 global $wgDeleteRevisionsLimit, $wgLang; 01857 01858 if ( $action == 'protect' ) { 01859 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $doExpensiveQueries, true ) ) ) { 01860 // If they can't edit, they shouldn't protect. 01861 $errors[] = array( 'protect-cantedit' ); 01862 } 01863 } elseif ( $action == 'create' ) { 01864 $title_protection = $this->getTitleProtection(); 01865 if( $title_protection ) { 01866 if( $title_protection['pt_create_perm'] == 'sysop' ) { 01867 $title_protection['pt_create_perm'] = 'protect'; // B/C 01868 } 01869 if( $title_protection['pt_create_perm'] == '' || 01870 !$user->isAllowed( $title_protection['pt_create_perm'] ) ) 01871 { 01872 $errors[] = array( 'titleprotected', User::whoIs( $title_protection['pt_user'] ), $title_protection['pt_reason'] ); 01873 } 01874 } 01875 } elseif ( $action == 'move' ) { 01876 // Check for immobile pages 01877 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 01878 // Specific message for this case 01879 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 01880 } elseif ( !$this->isMovable() ) { 01881 // Less specific message for rarer cases 01882 $errors[] = array( 'immobile-source-page' ); 01883 } 01884 } elseif ( $action == 'move-target' ) { 01885 if ( !MWNamespace::isMovable( $this->mNamespace ) ) { 01886 $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); 01887 } elseif ( !$this->isMovable() ) { 01888 $errors[] = array( 'immobile-target-page' ); 01889 } 01890 } elseif ( $action == 'delete' ) { 01891 if ( $doExpensiveQueries && $wgDeleteRevisionsLimit 01892 && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion() ) 01893 { 01894 $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ); 01895 } 01896 } 01897 return $errors; 01898 } 01899 01911 private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) { 01912 // Account creation blocks handled at userlogin. 01913 // Unblocking handled in SpecialUnblock 01914 if( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) { 01915 return $errors; 01916 } 01917 01918 global $wgContLang, $wgLang, $wgEmailConfirmToEdit; 01919 01920 if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { 01921 $errors[] = array( 'confirmedittext' ); 01922 } 01923 01924 if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) { 01925 // Don't block the user from editing their own talk page unless they've been 01926 // explicitly blocked from that too. 01927 } elseif( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) { 01928 $block = $user->mBlock; 01929 01930 // This is from OutputPage::blockedPage 01931 // Copied at r23888 by werdna 01932 01933 $id = $user->blockedBy(); 01934 $reason = $user->blockedFor(); 01935 if ( $reason == '' ) { 01936 $reason = wfMsg( 'blockednoreason' ); 01937 } 01938 $ip = $user->getRequest()->getIP(); 01939 01940 if ( is_numeric( $id ) ) { 01941 $name = User::whoIs( $id ); 01942 } else { 01943 $name = $id; 01944 } 01945 01946 $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; 01947 $blockid = $block->getId(); 01948 $blockExpiry = $user->mBlock->mExpiry; 01949 $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true ); 01950 if ( $blockExpiry == 'infinity' ) { 01951 $blockExpiry = wfMessage( 'infiniteblock' )->text(); 01952 } else { 01953 $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); 01954 } 01955 01956 $intended = strval( $user->mBlock->getTarget() ); 01957 01958 $errors[] = array( ( $block->mAuto ? 'autoblockedtext' : 'blockedtext' ), $link, $reason, $ip, $name, 01959 $blockid, $blockExpiry, $intended, $blockTimestamp ); 01960 } 01961 01962 return $errors; 01963 } 01964 01976 private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { 01977 global $wgWhitelistRead, $wgGroupPermissions, $wgRevokePermissions; 01978 static $useShortcut = null; 01979 01980 # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below 01981 if ( is_null( $useShortcut ) ) { 01982 $useShortcut = true; 01983 if ( empty( $wgGroupPermissions['*']['read'] ) ) { 01984 # Not a public wiki, so no shortcut 01985 $useShortcut = false; 01986 } elseif ( !empty( $wgRevokePermissions ) ) { 01993 foreach ( $wgRevokePermissions as $perms ) { 01994 if ( !empty( $perms['read'] ) ) { 01995 # We might be removing the read right from the user, so no shortcut 01996 $useShortcut = false; 01997 break; 01998 } 01999 } 02000 } 02001 } 02002 02003 $whitelisted = false; 02004 if ( $useShortcut ) { 02005 # Shortcut for public wikis, allows skipping quite a bit of code 02006 $whitelisted = true; 02007 } elseif ( $user->isAllowed( 'read' ) ) { 02008 # If the user is allowed to read pages, he is allowed to read all pages 02009 $whitelisted = true; 02010 } elseif ( $this->isSpecial( 'Userlogin' ) 02011 || $this->isSpecial( 'ChangePassword' ) 02012 || $this->isSpecial( 'PasswordReset' ) 02013 ) { 02014 # Always grant access to the login page. 02015 # Even anons need to be able to log in. 02016 $whitelisted = true; 02017 } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) { 02018 # Time to check the whitelist 02019 # Only do these checks is there's something to check against 02020 $name = $this->getPrefixedText(); 02021 $dbName = $this->getPrefixedDBKey(); 02022 02023 // Check with and without underscores 02024 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) { 02025 # Check for explicit whitelisting 02026 $whitelisted = true; 02027 } elseif ( $this->getNamespace() == NS_MAIN ) { 02028 # Old settings might have the title prefixed with 02029 # a colon for main-namespace pages 02030 if ( in_array( ':' . $name, $wgWhitelistRead ) ) { 02031 $whitelisted = true; 02032 } 02033 } elseif ( $this->isSpecialPage() ) { 02034 # If it's a special page, ditch the subpage bit and check again 02035 $name = $this->getDBkey(); 02036 list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name ); 02037 if ( $name !== false ) { 02038 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); 02039 if ( in_array( $pure, $wgWhitelistRead, true ) ) { 02040 $whitelisted = true; 02041 } 02042 } 02043 } 02044 } 02045 02046 if ( !$whitelisted ) { 02047 # If the title is not whitelisted, give extensions a chance to do so... 02048 wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); 02049 if ( !$whitelisted ) { 02050 $errors[] = $this->missingPermissionError( $action, $short ); 02051 } 02052 } 02053 02054 return $errors; 02055 } 02056 02065 private function missingPermissionError( $action, $short ) { 02066 // We avoid expensive display logic for quickUserCan's and such 02067 if ( $short ) { 02068 return array( 'badaccess-group0' ); 02069 } 02070 02071 $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), 02072 User::getGroupsWithPermission( $action ) ); 02073 02074 if ( count( $groups ) ) { 02075 global $wgLang; 02076 return array( 02077 'badaccess-groups', 02078 $wgLang->commaList( $groups ), 02079 count( $groups ) 02080 ); 02081 } else { 02082 return array( 'badaccess-group0' ); 02083 } 02084 } 02085 02097 protected function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true, $short = false ) { 02098 wfProfileIn( __METHOD__ ); 02099 02100 # Read has special handling 02101 if ( $action == 'read' ) { 02102 $checks = array( 02103 'checkPermissionHooks', 02104 'checkReadPermissions', 02105 ); 02106 } else { 02107 $checks = array( 02108 'checkQuickPermissions', 02109 'checkPermissionHooks', 02110 'checkSpecialsAndNSPermissions', 02111 'checkCSSandJSPermissions', 02112 'checkPageRestrictions', 02113 'checkCascadingSourcesRestrictions', 02114 'checkActionPermissions', 02115 'checkUserBlock' 02116 ); 02117 } 02118 02119 $errors = array(); 02120 while( count( $checks ) > 0 && 02121 !( $short && count( $errors ) > 0 ) ) { 02122 $method = array_shift( $checks ); 02123 $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); 02124 } 02125 02126 wfProfileOut( __METHOD__ ); 02127 return $errors; 02128 } 02129 02137 public function userCanEditCssSubpage() { 02138 global $wgUser; 02139 wfDeprecated( __METHOD__, '1.19' ); 02140 return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) ) 02141 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); 02142 } 02143 02151 public function userCanEditJsSubpage() { 02152 global $wgUser; 02153 wfDeprecated( __METHOD__, '1.19' ); 02154 return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) ) 02155 || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); 02156 } 02157 02165 public static function getFilteredRestrictionTypes( $exists = true ) { 02166 global $wgRestrictionTypes; 02167 $types = $wgRestrictionTypes; 02168 if ( $exists ) { 02169 # Remove the create restriction for existing titles 02170 $types = array_diff( $types, array( 'create' ) ); 02171 } else { 02172 # Only the create and upload restrictions apply to non-existing titles 02173 $types = array_intersect( $types, array( 'create', 'upload' ) ); 02174 } 02175 return $types; 02176 } 02177 02183 public function getRestrictionTypes() { 02184 if ( $this->isSpecialPage() ) { 02185 return array(); 02186 } 02187 02188 $types = self::getFilteredRestrictionTypes( $this->exists() ); 02189 02190 if ( $this->getNamespace() != NS_FILE ) { 02191 # Remove the upload restriction for non-file titles 02192 $types = array_diff( $types, array( 'upload' ) ); 02193 } 02194 02195 wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); 02196 02197 wfDebug( __METHOD__ . ': applicable restrictions to [[' . 02198 $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); 02199 02200 return $types; 02201 } 02202 02210 private function getTitleProtection() { 02211 // Can't protect pages in special namespaces 02212 if ( $this->getNamespace() < 0 ) { 02213 return false; 02214 } 02215 02216 // Can't protect pages that exist. 02217 if ( $this->exists() ) { 02218 return false; 02219 } 02220 02221 if ( !isset( $this->mTitleProtection ) ) { 02222 $dbr = wfGetDB( DB_SLAVE ); 02223 $res = $dbr->select( 'protected_titles', '*', 02224 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02225 __METHOD__ ); 02226 02227 // fetchRow returns false if there are no rows. 02228 $this->mTitleProtection = $dbr->fetchRow( $res ); 02229 } 02230 return $this->mTitleProtection; 02231 } 02232 02242 public function updateTitleProtection( $create_perm, $reason, $expiry ) { 02243 wfDeprecated( __METHOD__, '1.19' ); 02244 02245 global $wgUser; 02246 02247 $limit = array( 'create' => $create_perm ); 02248 $expiry = array( 'create' => $expiry ); 02249 02250 $page = WikiPage::factory( $this ); 02251 $status = $page->doUpdateRestrictions( $limit, $expiry, false, $reason, $wgUser ); 02252 02253 return $status->isOK(); 02254 } 02255 02259 public function deleteTitleProtection() { 02260 $dbw = wfGetDB( DB_MASTER ); 02261 02262 $dbw->delete( 02263 'protected_titles', 02264 array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), 02265 __METHOD__ 02266 ); 02267 $this->mTitleProtection = false; 02268 } 02269 02276 public function isSemiProtected( $action = 'edit' ) { 02277 if ( $this->exists() ) { 02278 $restrictions = $this->getRestrictions( $action ); 02279 if ( count( $restrictions ) > 0 ) { 02280 foreach ( $restrictions as $restriction ) { 02281 if ( strtolower( $restriction ) != 'autoconfirmed' ) { 02282 return false; 02283 } 02284 } 02285 } else { 02286 # Not protected 02287 return false; 02288 } 02289 return true; 02290 } else { 02291 # If it doesn't exist, it can't be protected 02292 return false; 02293 } 02294 } 02295 02303 public function isProtected( $action = '' ) { 02304 global $wgRestrictionLevels; 02305 02306 $restrictionTypes = $this->getRestrictionTypes(); 02307 02308 # Special pages have inherent protection 02309 if( $this->isSpecialPage() ) { 02310 return true; 02311 } 02312 02313 # Check regular protection levels 02314 foreach ( $restrictionTypes as $type ) { 02315 if ( $action == $type || $action == '' ) { 02316 $r = $this->getRestrictions( $type ); 02317 foreach ( $wgRestrictionLevels as $level ) { 02318 if ( in_array( $level, $r ) && $level != '' ) { 02319 return true; 02320 } 02321 } 02322 } 02323 } 02324 02325 return false; 02326 } 02327 02335 public function isNamespaceProtected( User $user ) { 02336 global $wgNamespaceProtection; 02337 02338 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) { 02339 foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) { 02340 if ( $right != '' && !$user->isAllowed( $right ) ) { 02341 return true; 02342 } 02343 } 02344 } 02345 return false; 02346 } 02347 02353 public function isCascadeProtected() { 02354 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); 02355 return ( $sources > 0 ); 02356 } 02357 02368 public function getCascadeProtectionSources( $getPages = true ) { 02369 global $wgContLang; 02370 $pagerestrictions = array(); 02371 02372 if ( isset( $this->mCascadeSources ) && $getPages ) { 02373 return array( $this->mCascadeSources, $this->mCascadingRestrictions ); 02374 } elseif ( isset( $this->mHasCascadingRestrictions ) && !$getPages ) { 02375 return array( $this->mHasCascadingRestrictions, $pagerestrictions ); 02376 } 02377 02378 wfProfileIn( __METHOD__ ); 02379 02380 $dbr = wfGetDB( DB_SLAVE ); 02381 02382 if ( $this->getNamespace() == NS_FILE ) { 02383 $tables = array( 'imagelinks', 'page_restrictions' ); 02384 $where_clauses = array( 02385 'il_to' => $this->getDBkey(), 02386 'il_from=pr_page', 02387 'pr_cascade' => 1 02388 ); 02389 } else { 02390 $tables = array( 'templatelinks', 'page_restrictions' ); 02391 $where_clauses = array( 02392 'tl_namespace' => $this->getNamespace(), 02393 'tl_title' => $this->getDBkey(), 02394 'tl_from=pr_page', 02395 'pr_cascade' => 1 02396 ); 02397 } 02398 02399 if ( $getPages ) { 02400 $cols = array( 'pr_page', 'page_namespace', 'page_title', 02401 'pr_expiry', 'pr_type', 'pr_level' ); 02402 $where_clauses[] = 'page_id=pr_page'; 02403 $tables[] = 'page'; 02404 } else { 02405 $cols = array( 'pr_expiry' ); 02406 } 02407 02408 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ ); 02409 02410 $sources = $getPages ? array() : false; 02411 $now = wfTimestampNow(); 02412 $purgeExpired = false; 02413 02414 foreach ( $res as $row ) { 02415 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02416 if ( $expiry > $now ) { 02417 if ( $getPages ) { 02418 $page_id = $row->pr_page; 02419 $page_ns = $row->page_namespace; 02420 $page_title = $row->page_title; 02421 $sources[$page_id] = Title::makeTitle( $page_ns, $page_title ); 02422 # Add groups needed for each restriction type if its not already there 02423 # Make sure this restriction type still exists 02424 02425 if ( !isset( $pagerestrictions[$row->pr_type] ) ) { 02426 $pagerestrictions[$row->pr_type] = array(); 02427 } 02428 02429 if ( isset( $pagerestrictions[$row->pr_type] ) && 02430 !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] ) ) { 02431 $pagerestrictions[$row->pr_type][] = $row->pr_level; 02432 } 02433 } else { 02434 $sources = true; 02435 } 02436 } else { 02437 // Trigger lazy purge of expired restrictions from the db 02438 $purgeExpired = true; 02439 } 02440 } 02441 if ( $purgeExpired ) { 02442 Title::purgeExpiredRestrictions(); 02443 } 02444 02445 if ( $getPages ) { 02446 $this->mCascadeSources = $sources; 02447 $this->mCascadingRestrictions = $pagerestrictions; 02448 } else { 02449 $this->mHasCascadingRestrictions = $sources; 02450 } 02451 02452 wfProfileOut( __METHOD__ ); 02453 return array( $sources, $pagerestrictions ); 02454 } 02455 02462 public function getRestrictions( $action ) { 02463 if ( !$this->mRestrictionsLoaded ) { 02464 $this->loadRestrictions(); 02465 } 02466 return isset( $this->mRestrictions[$action] ) 02467 ? $this->mRestrictions[$action] 02468 : array(); 02469 } 02470 02477 public function getRestrictionExpiry( $action ) { 02478 if ( !$this->mRestrictionsLoaded ) { 02479 $this->loadRestrictions(); 02480 } 02481 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; 02482 } 02483 02489 function areRestrictionsCascading() { 02490 if ( !$this->mRestrictionsLoaded ) { 02491 $this->loadRestrictions(); 02492 } 02493 02494 return $this->mCascadeRestriction; 02495 } 02496 02504 private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) { 02505 $rows = array(); 02506 02507 foreach ( $res as $row ) { 02508 $rows[] = $row; 02509 } 02510 02511 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); 02512 } 02513 02523 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { 02524 global $wgContLang; 02525 $dbr = wfGetDB( DB_SLAVE ); 02526 02527 $restrictionTypes = $this->getRestrictionTypes(); 02528 02529 foreach ( $restrictionTypes as $type ) { 02530 $this->mRestrictions[$type] = array(); 02531 $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); 02532 } 02533 02534 $this->mCascadeRestriction = false; 02535 02536 # Backwards-compatibility: also load the restrictions from the page record (old format). 02537 02538 if ( $oldFashionedRestrictions === null ) { 02539 $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', 02540 array( 'page_id' => $this->getArticleId() ), __METHOD__ ); 02541 } 02542 02543 if ( $oldFashionedRestrictions != '' ) { 02544 02545 foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { 02546 $temp = explode( '=', trim( $restrict ) ); 02547 if ( count( $temp ) == 1 ) { 02548 // old old format should be treated as edit/move restriction 02549 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) ); 02550 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) ); 02551 } else { 02552 $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) ); 02553 } 02554 } 02555 02556 $this->mOldRestrictions = true; 02557 02558 } 02559 02560 if ( count( $rows ) ) { 02561 # Current system - load second to make them override. 02562 $now = wfTimestampNow(); 02563 $purgeExpired = false; 02564 02565 # Cycle through all the restrictions. 02566 foreach ( $rows as $row ) { 02567 02568 // Don't take care of restrictions types that aren't allowed 02569 if ( !in_array( $row->pr_type, $restrictionTypes ) ) 02570 continue; 02571 02572 // This code should be refactored, now that it's being used more generally, 02573 // But I don't really see any harm in leaving it in Block for now -werdna 02574 $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); 02575 02576 // Only apply the restrictions if they haven't expired! 02577 if ( !$expiry || $expiry > $now ) { 02578 $this->mRestrictionsExpiry[$row->pr_type] = $expiry; 02579 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) ); 02580 02581 $this->mCascadeRestriction |= $row->pr_cascade; 02582 } else { 02583 // Trigger a lazy purge of expired restrictions 02584 $purgeExpired = true; 02585 } 02586 } 02587 02588 if ( $purgeExpired ) { 02589 Title::purgeExpiredRestrictions(); 02590 } 02591 } 02592 02593 $this->mRestrictionsLoaded = true; 02594 } 02595 02602 public function loadRestrictions( $oldFashionedRestrictions = null ) { 02603 global $wgContLang; 02604 if ( !$this->mRestrictionsLoaded ) { 02605 if ( $this->exists() ) { 02606 $dbr = wfGetDB( DB_SLAVE ); 02607 02608 $res = $dbr->select( 02609 'page_restrictions', 02610 '*', 02611 array( 'pr_page' => $this->getArticleId() ), 02612 __METHOD__ 02613 ); 02614 02615 $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); 02616 } else { 02617 $title_protection = $this->getTitleProtection(); 02618 02619 if ( $title_protection ) { 02620 $now = wfTimestampNow(); 02621 $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW ); 02622 02623 if ( !$expiry || $expiry > $now ) { 02624 // Apply the restrictions 02625 $this->mRestrictionsExpiry['create'] = $expiry; 02626 $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) ); 02627 } else { // Get rid of the old restrictions 02628 Title::purgeExpiredRestrictions(); 02629 $this->mTitleProtection = false; 02630 } 02631 } else { 02632 $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); 02633 } 02634 $this->mRestrictionsLoaded = true; 02635 } 02636 } 02637 } 02638 02643 public function flushRestrictions() { 02644 $this->mRestrictionsLoaded = false; 02645 $this->mTitleProtection = null; 02646 } 02647 02651 static function purgeExpiredRestrictions() { 02652 $dbw = wfGetDB( DB_MASTER ); 02653 $dbw->delete( 02654 'page_restrictions', 02655 array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02656 __METHOD__ 02657 ); 02658 02659 $dbw->delete( 02660 'protected_titles', 02661 array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), 02662 __METHOD__ 02663 ); 02664 } 02665 02671 public function hasSubpages() { 02672 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { 02673 # Duh 02674 return false; 02675 } 02676 02677 # We dynamically add a member variable for the purpose of this method 02678 # alone to cache the result. There's no point in having it hanging 02679 # around uninitialized in every Title object; therefore we only add it 02680 # if needed and don't declare it statically. 02681 if ( isset( $this->mHasSubpages ) ) { 02682 return $this->mHasSubpages; 02683 } 02684 02685 $subpages = $this->getSubpages( 1 ); 02686 if ( $subpages instanceof TitleArray ) { 02687 return $this->mHasSubpages = (bool)$subpages->count(); 02688 } 02689 return $this->mHasSubpages = false; 02690 } 02691 02699 public function getSubpages( $limit = -1 ) { 02700 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 02701 return array(); 02702 } 02703 02704 $dbr = wfGetDB( DB_SLAVE ); 02705 $conds['page_namespace'] = $this->getNamespace(); 02706 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); 02707 $options = array(); 02708 if ( $limit > -1 ) { 02709 $options['LIMIT'] = $limit; 02710 } 02711 return $this->mSubpages = TitleArray::newFromResult( 02712 $dbr->select( 'page', 02713 array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), 02714 $conds, 02715 __METHOD__, 02716 $options 02717 ) 02718 ); 02719 } 02720 02726 public function isDeleted() { 02727 if ( $this->getNamespace() < 0 ) { 02728 $n = 0; 02729 } else { 02730 $dbr = wfGetDB( DB_SLAVE ); 02731 02732 $n = $dbr->selectField( 'archive', 'COUNT(*)', 02733 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02734 __METHOD__ 02735 ); 02736 if ( $this->getNamespace() == NS_FILE ) { 02737 $n += $dbr->selectField( 'filearchive', 'COUNT(*)', 02738 array( 'fa_name' => $this->getDBkey() ), 02739 __METHOD__ 02740 ); 02741 } 02742 } 02743 return (int)$n; 02744 } 02745 02751 public function isDeletedQuick() { 02752 if ( $this->getNamespace() < 0 ) { 02753 return false; 02754 } 02755 $dbr = wfGetDB( DB_SLAVE ); 02756 $deleted = (bool)$dbr->selectField( 'archive', '1', 02757 array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), 02758 __METHOD__ 02759 ); 02760 if ( !$deleted && $this->getNamespace() == NS_FILE ) { 02761 $deleted = (bool)$dbr->selectField( 'filearchive', '1', 02762 array( 'fa_name' => $this->getDBkey() ), 02763 __METHOD__ 02764 ); 02765 } 02766 return $deleted; 02767 } 02768 02777 public function getArticleID( $flags = 0 ) { 02778 if ( $this->getNamespace() < 0 ) { 02779 return $this->mArticleID = 0; 02780 } 02781 $linkCache = LinkCache::singleton(); 02782 if ( $flags & self::GAID_FOR_UPDATE ) { 02783 $oldUpdate = $linkCache->forUpdate( true ); 02784 $linkCache->clearLink( $this ); 02785 $this->mArticleID = $linkCache->addLinkObj( $this ); 02786 $linkCache->forUpdate( $oldUpdate ); 02787 } else { 02788 if ( -1 == $this->mArticleID ) { 02789 $this->mArticleID = $linkCache->addLinkObj( $this ); 02790 } 02791 } 02792 return $this->mArticleID; 02793 } 02794 02802 public function isRedirect( $flags = 0 ) { 02803 if ( !is_null( $this->mRedirect ) ) { 02804 return $this->mRedirect; 02805 } 02806 # Calling getArticleID() loads the field from cache as needed 02807 if ( !$this->getArticleID( $flags ) ) { 02808 return $this->mRedirect = false; 02809 } 02810 $linkCache = LinkCache::singleton(); 02811 $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' ); 02812 02813 return $this->mRedirect; 02814 } 02815 02823 public function getLength( $flags = 0 ) { 02824 if ( $this->mLength != -1 ) { 02825 return $this->mLength; 02826 } 02827 # Calling getArticleID() loads the field from cache as needed 02828 if ( !$this->getArticleID( $flags ) ) { 02829 return $this->mLength = 0; 02830 } 02831 $linkCache = LinkCache::singleton(); 02832 $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) ); 02833 02834 return $this->mLength; 02835 } 02836 02843 public function getLatestRevID( $flags = 0 ) { 02844 if ( $this->mLatestID !== false ) { 02845 return intval( $this->mLatestID ); 02846 } 02847 # Calling getArticleID() loads the field from cache as needed 02848 if ( !$this->getArticleID( $flags ) ) { 02849 return $this->mLatestID = 0; 02850 } 02851 $linkCache = LinkCache::singleton(); 02852 $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) ); 02853 02854 return $this->mLatestID; 02855 } 02856 02867 public function resetArticleID( $newid ) { 02868 $linkCache = LinkCache::singleton(); 02869 $linkCache->clearLink( $this ); 02870 02871 if ( $newid === false ) { 02872 $this->mArticleID = -1; 02873 } else { 02874 $this->mArticleID = intval( $newid ); 02875 } 02876 $this->mRestrictionsLoaded = false; 02877 $this->mRestrictions = array(); 02878 $this->mRedirect = null; 02879 $this->mLength = -1; 02880 $this->mLatestID = false; 02881 $this->mEstimateRevisions = null; 02882 } 02883 02891 public static function capitalize( $text, $ns = NS_MAIN ) { 02892 global $wgContLang; 02893 02894 if ( MWNamespace::isCapitalized( $ns ) ) { 02895 return $wgContLang->ucfirst( $text ); 02896 } else { 02897 return $text; 02898 } 02899 } 02900 02912 private function secureAndSplit() { 02913 global $wgContLang, $wgLocalInterwiki; 02914 02915 # Initialisation 02916 $this->mInterwiki = $this->mFragment = ''; 02917 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN 02918 02919 $dbkey = $this->mDbkeyform; 02920 02921 # Strip Unicode bidi override characters. 02922 # Sometimes they slip into cut-n-pasted page titles, where the 02923 # override chars get included in list displays. 02924 $dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey ); 02925 02926 # Clean up whitespace 02927 # Note: use of the /u option on preg_replace here will cause 02928 # input with invalid UTF-8 sequences to be nullified out in PHP 5.2.x, 02929 # conveniently disabling them. 02930 $dbkey = preg_replace( '/[ _\xA0\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u', '_', $dbkey ); 02931 $dbkey = trim( $dbkey, '_' ); 02932 02933 if ( $dbkey == '' ) { 02934 return false; 02935 } 02936 02937 if ( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) { 02938 # Contained illegal UTF-8 sequences or forbidden Unicode chars. 02939 return false; 02940 } 02941 02942 $this->mDbkeyform = $dbkey; 02943 02944 # Initial colon indicates main namespace rather than specified default 02945 # but should not create invalid {ns,title} pairs such as {0,Project:Foo} 02946 if ( ':' == $dbkey[0] ) { 02947 $this->mNamespace = NS_MAIN; 02948 $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing 02949 $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace 02950 } 02951 02952 # Namespace or interwiki prefix 02953 $firstPass = true; 02954 $prefixRegexp = "/^(.+?)_*:_*(.*)$/S"; 02955 do { 02956 $m = array(); 02957 if ( preg_match( $prefixRegexp, $dbkey, $m ) ) { 02958 $p = $m[1]; 02959 if ( ( $ns = $wgContLang->getNsIndex( $p ) ) !== false ) { 02960 # Ordinary namespace 02961 $dbkey = $m[2]; 02962 $this->mNamespace = $ns; 02963 # For Talk:X pages, check if X has a "namespace" prefix 02964 if ( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) { 02965 if ( $wgContLang->getNsIndex( $x[1] ) ) { 02966 # Disallow Talk:File:x type titles... 02967 return false; 02968 } elseif ( Interwiki::isValidInterwiki( $x[1] ) ) { 02969 # Disallow Talk:Interwiki:x type titles... 02970 return false; 02971 } 02972 } 02973 } elseif ( Interwiki::isValidInterwiki( $p ) ) { 02974 if ( !$firstPass ) { 02975 # Can't make a local interwiki link to an interwiki link. 02976 # That's just crazy! 02977 return false; 02978 } 02979 02980 # Interwiki link 02981 $dbkey = $m[2]; 02982 $this->mInterwiki = $wgContLang->lc( $p ); 02983 02984 # Redundant interwiki prefix to the local wiki 02985 if ( $wgLocalInterwiki !== false 02986 && 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) 02987 { 02988 if ( $dbkey == '' ) { 02989 # Can't have an empty self-link 02990 return false; 02991 } 02992 $this->mInterwiki = ''; 02993 $firstPass = false; 02994 # Do another namespace split... 02995 continue; 02996 } 02997 02998 # If there's an initial colon after the interwiki, that also 02999 # resets the default namespace 03000 if ( $dbkey !== '' && $dbkey[0] == ':' ) { 03001 $this->mNamespace = NS_MAIN; 03002 $dbkey = substr( $dbkey, 1 ); 03003 } 03004 } 03005 # If there's no recognized interwiki or namespace, 03006 # then let the colon expression be part of the title. 03007 } 03008 break; 03009 } while ( true ); 03010 03011 # We already know that some pages won't be in the database! 03012 if ( $this->mInterwiki != '' || NS_SPECIAL == $this->mNamespace ) { 03013 $this->mArticleID = 0; 03014 } 03015 $fragment = strstr( $dbkey, '#' ); 03016 if ( false !== $fragment ) { 03017 $this->setFragment( $fragment ); 03018 $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) ); 03019 # remove whitespace again: prevents "Foo_bar_#" 03020 # becoming "Foo_bar_" 03021 $dbkey = preg_replace( '/_*$/', '', $dbkey ); 03022 } 03023 03024 # Reject illegal characters. 03025 $rxTc = self::getTitleInvalidRegex(); 03026 if ( preg_match( $rxTc, $dbkey ) ) { 03027 return false; 03028 } 03029 03030 # Pages with "/./" or "/../" appearing in the URLs will often be un- 03031 # reachable due to the way web browsers deal with 'relative' URLs. 03032 # Also, they conflict with subpage syntax. Forbid them explicitly. 03033 if ( strpos( $dbkey, '.' ) !== false && 03034 ( $dbkey === '.' || $dbkey === '..' || 03035 strpos( $dbkey, './' ) === 0 || 03036 strpos( $dbkey, '../' ) === 0 || 03037 strpos( $dbkey, '/./' ) !== false || 03038 strpos( $dbkey, '/../' ) !== false || 03039 substr( $dbkey, -2 ) == '/.' || 03040 substr( $dbkey, -3 ) == '/..' ) ) 03041 { 03042 return false; 03043 } 03044 03045 # Magic tilde sequences? Nu-uh! 03046 if ( strpos( $dbkey, '~~~' ) !== false ) { 03047 return false; 03048 } 03049 03050 # Limit the size of titles to 255 bytes. This is typically the size of the 03051 # underlying database field. We make an exception for special pages, which 03052 # don't need to be stored in the database, and may edge over 255 bytes due 03053 # to subpage syntax for long titles, e.g. [[Special:Block/Long name]] 03054 if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) || 03055 strlen( $dbkey ) > 512 ) 03056 { 03057 return false; 03058 } 03059 03060 # Normally, all wiki links are forced to have an initial capital letter so [[foo]] 03061 # and [[Foo]] point to the same place. Don't force it for interwikis, since the 03062 # other site might be case-sensitive. 03063 $this->mUserCaseDBKey = $dbkey; 03064 if ( $this->mInterwiki == '' ) { 03065 $dbkey = self::capitalize( $dbkey, $this->mNamespace ); 03066 } 03067 03068 # Can't make a link to a namespace alone... "empty" local links can only be 03069 # self-links with a fragment identifier. 03070 if ( $dbkey == '' && $this->mInterwiki == '' && $this->mNamespace != NS_MAIN ) { 03071 return false; 03072 } 03073 03074 // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles. 03075 // IP names are not allowed for accounts, and can only be referring to 03076 // edits from the IP. Given '::' abbreviations and caps/lowercaps, 03077 // there are numerous ways to present the same IP. Having sp:contribs scan 03078 // them all is silly and having some show the edits and others not is 03079 // inconsistent. Same for talk/userpages. Keep them normalized instead. 03080 $dbkey = ( $this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK ) 03081 ? IP::sanitizeIP( $dbkey ) 03082 : $dbkey; 03083 03084 // Any remaining initial :s are illegal. 03085 if ( $dbkey !== '' && ':' == $dbkey[0] ) { 03086 return false; 03087 } 03088 03089 # Fill fields 03090 $this->mDbkeyform = $dbkey; 03091 $this->mUrlform = wfUrlencode( $dbkey ); 03092 03093 $this->mTextform = str_replace( '_', ' ', $dbkey ); 03094 03095 return true; 03096 } 03097 03110 public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03111 if ( count( $options ) > 0 ) { 03112 $db = wfGetDB( DB_MASTER ); 03113 } else { 03114 $db = wfGetDB( DB_SLAVE ); 03115 } 03116 03117 $res = $db->select( 03118 array( 'page', $table ), 03119 array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), 03120 array( 03121 "{$prefix}_from=page_id", 03122 "{$prefix}_namespace" => $this->getNamespace(), 03123 "{$prefix}_title" => $this->getDBkey() ), 03124 __METHOD__, 03125 $options 03126 ); 03127 03128 $retVal = array(); 03129 if ( $res->numRows() ) { 03130 $linkCache = LinkCache::singleton(); 03131 foreach ( $res as $row ) { 03132 $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ); 03133 if ( $titleObj ) { 03134 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03135 $retVal[] = $titleObj; 03136 } 03137 } 03138 } 03139 return $retVal; 03140 } 03141 03152 public function getTemplateLinksTo( $options = array() ) { 03153 return $this->getLinksTo( $options, 'templatelinks', 'tl' ); 03154 } 03155 03168 public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { 03169 $id = $this->getArticleId(); 03170 03171 # If the page doesn't exist; there can't be any link from this page 03172 if ( !$id ) { 03173 return array(); 03174 } 03175 03176 if ( count( $options ) > 0 ) { 03177 $db = wfGetDB( DB_MASTER ); 03178 } else { 03179 $db = wfGetDB( DB_SLAVE ); 03180 } 03181 03182 $namespaceFiled = "{$prefix}_namespace"; 03183 $titleField = "{$prefix}_title"; 03184 03185 $res = $db->select( 03186 array( $table, 'page' ), 03187 array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), 03188 array( "{$prefix}_from" => $id ), 03189 __METHOD__, 03190 $options, 03191 array( 'page' => array( 'LEFT JOIN', array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) ) ) 03192 ); 03193 03194 $retVal = array(); 03195 if ( $res->numRows() ) { 03196 $linkCache = LinkCache::singleton(); 03197 foreach ( $res as $row ) { 03198 $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); 03199 if ( $titleObj ) { 03200 if ( $row->page_id ) { 03201 $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); 03202 } else { 03203 $linkCache->addBadLinkObj( $titleObj ); 03204 } 03205 $retVal[] = $titleObj; 03206 } 03207 } 03208 } 03209 return $retVal; 03210 } 03211 03222 public function getTemplateLinksFrom( $options = array() ) { 03223 return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); 03224 } 03225 03232 public function getBrokenLinksFrom() { 03233 if ( $this->getArticleId() == 0 ) { 03234 # All links from article ID 0 are false positives 03235 return array(); 03236 } 03237 03238 $dbr = wfGetDB( DB_SLAVE ); 03239 $res = $dbr->select( 03240 array( 'page', 'pagelinks' ), 03241 array( 'pl_namespace', 'pl_title' ), 03242 array( 03243 'pl_from' => $this->getArticleId(), 03244 'page_namespace IS NULL' 03245 ), 03246 __METHOD__, array(), 03247 array( 03248 'page' => array( 03249 'LEFT JOIN', 03250 array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) 03251 ) 03252 ) 03253 ); 03254 03255 $retVal = array(); 03256 foreach ( $res as $row ) { 03257 $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); 03258 } 03259 return $retVal; 03260 } 03261 03262 03269 public function getSquidURLs() { 03270 global $wgContLang; 03271 03272 $urls = array( 03273 $this->getInternalURL(), 03274 $this->getInternalURL( 'action=history' ) 03275 ); 03276 03277 // purge variant urls as well 03278 if ( $wgContLang->hasVariants() ) { 03279 $variants = $wgContLang->getVariants(); 03280 foreach ( $variants as $vCode ) { 03281 $urls[] = $this->getInternalURL( '', $vCode ); 03282 } 03283 } 03284 03285 return $urls; 03286 } 03287 03291 public function purgeSquid() { 03292 global $wgUseSquid; 03293 if ( $wgUseSquid ) { 03294 $urls = $this->getSquidURLs(); 03295 $u = new SquidUpdate( $urls ); 03296 $u->doUpdate(); 03297 } 03298 } 03299 03306 public function moveNoAuth( &$nt ) { 03307 return $this->moveTo( $nt, false ); 03308 } 03309 03320 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { 03321 global $wgUser; 03322 03323 $errors = array(); 03324 if ( !$nt ) { 03325 // Normally we'd add this to $errors, but we'll get 03326 // lots of syntax errors if $nt is not an object 03327 return array( array( 'badtitletext' ) ); 03328 } 03329 if ( $this->equals( $nt ) ) { 03330 $errors[] = array( 'selfmove' ); 03331 } 03332 if ( !$this->isMovable() ) { 03333 $errors[] = array( 'immobile-source-namespace', $this->getNsText() ); 03334 } 03335 if ( $nt->getInterwiki() != '' ) { 03336 $errors[] = array( 'immobile-target-namespace-iw' ); 03337 } 03338 if ( !$nt->isMovable() ) { 03339 $errors[] = array( 'immobile-target-namespace', $nt->getNsText() ); 03340 } 03341 03342 $oldid = $this->getArticleID(); 03343 $newid = $nt->getArticleID(); 03344 03345 if ( strlen( $nt->getDBkey() ) < 1 ) { 03346 $errors[] = array( 'articleexists' ); 03347 } 03348 if ( ( $this->getDBkey() == '' ) || 03349 ( !$oldid ) || 03350 ( $nt->getDBkey() == '' ) ) { 03351 $errors[] = array( 'badarticleerror' ); 03352 } 03353 03354 // Image-specific checks 03355 if ( $this->getNamespace() == NS_FILE ) { 03356 $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) ); 03357 } 03358 03359 if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) { 03360 $errors[] = array( 'nonfile-cannot-move-to-file' ); 03361 } 03362 03363 if ( $auth ) { 03364 $errors = wfMergeErrorArrays( $errors, 03365 $this->getUserPermissionsErrors( 'move', $wgUser ), 03366 $this->getUserPermissionsErrors( 'edit', $wgUser ), 03367 $nt->getUserPermissionsErrors( 'move-target', $wgUser ), 03368 $nt->getUserPermissionsErrors( 'edit', $wgUser ) ); 03369 } 03370 03371 $match = EditPage::matchSummarySpamRegex( $reason ); 03372 if ( $match !== false ) { 03373 // This is kind of lame, won't display nice 03374 $errors[] = array( 'spamprotectiontext' ); 03375 } 03376 03377 $err = null; 03378 if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) { 03379 $errors[] = array( 'hookaborted', $err ); 03380 } 03381 03382 # The move is allowed only if (1) the target doesn't exist, or 03383 # (2) the target is a redirect to the source, and has no history 03384 # (so we can undo bad moves right after they're done). 03385 03386 if ( 0 != $newid ) { # Target exists; check for validity 03387 if ( !$this->isValidMoveTarget( $nt ) ) { 03388 $errors[] = array( 'articleexists' ); 03389 } 03390 } else { 03391 $tp = $nt->getTitleProtection(); 03392 $right = ( $tp['pt_create_perm'] == 'sysop' ) ? 'protect' : $tp['pt_create_perm']; 03393 if ( $tp and !$wgUser->isAllowed( $right ) ) { 03394 $errors[] = array( 'cantmove-titleprotected' ); 03395 } 03396 } 03397 if ( empty( $errors ) ) { 03398 return true; 03399 } 03400 return $errors; 03401 } 03402 03408 protected function validateFileMoveOperation( $nt ) { 03409 global $wgUser; 03410 03411 $errors = array(); 03412 03413 // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below 03414 03415 $file = wfLocalFile( $this ); 03416 if ( $file->exists() ) { 03417 if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { 03418 $errors[] = array( 'imageinvalidfilename' ); 03419 } 03420 if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) { 03421 $errors[] = array( 'imagetypemismatch' ); 03422 } 03423 } 03424 03425 if ( $nt->getNamespace() != NS_FILE ) { 03426 $errors[] = array( 'imagenocrossnamespace' ); 03427 // From here we want to do checks on a file object, so if we can't 03428 // create one, we must return. 03429 return $errors; 03430 } 03431 03432 // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here 03433 03434 $destFile = wfLocalFile( $nt ); 03435 if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) { 03436 $errors[] = array( 'file-exists-sharedrepo' ); 03437 } 03438 03439 return $errors; 03440 } 03441 03453 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { 03454 global $wgUser; 03455 $err = $this->isValidMoveOperation( $nt, $auth, $reason ); 03456 if ( is_array( $err ) ) { 03457 // Auto-block user's IP if the account was "hard" blocked 03458 $wgUser->spreadAnyEditBlock(); 03459 return $err; 03460 } 03461 03462 // If it is a file, move it first. 03463 // It is done before all other moving stuff is done because it's hard to revert. 03464 $dbw = wfGetDB( DB_MASTER ); 03465 if ( $this->getNamespace() == NS_FILE ) { 03466 $file = wfLocalFile( $this ); 03467 if ( $file->exists() ) { 03468 $status = $file->move( $nt ); 03469 if ( !$status->isOk() ) { 03470 return $status->getErrorsArray(); 03471 } 03472 } 03473 // Clear RepoGroup process cache 03474 RepoGroup::singleton()->clearCache( $this ); 03475 RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache 03476 } 03477 03478 $dbw->begin(); # If $file was a LocalFile, its transaction would have closed our own. 03479 $pageid = $this->getArticleID( self::GAID_FOR_UPDATE ); 03480 $protected = $this->isProtected(); 03481 03482 // Do the actual move 03483 $err = $this->moveToInternal( $nt, $reason, $createRedirect ); 03484 if ( is_array( $err ) ) { 03485 # @todo FIXME: What about the File we have already moved? 03486 $dbw->rollback(); 03487 return $err; 03488 } 03489 03490 // Refresh the sortkey for this row. Be careful to avoid resetting 03491 // cl_timestamp, which may disturb time-based lists on some sites. 03492 $prefixes = $dbw->select( 03493 'categorylinks', 03494 array( 'cl_sortkey_prefix', 'cl_to' ), 03495 array( 'cl_from' => $pageid ), 03496 __METHOD__ 03497 ); 03498 foreach ( $prefixes as $prefixRow ) { 03499 $prefix = $prefixRow->cl_sortkey_prefix; 03500 $catTo = $prefixRow->cl_to; 03501 $dbw->update( 'categorylinks', 03502 array( 03503 'cl_sortkey' => Collation::singleton()->getSortKey( 03504 $nt->getCategorySortkey( $prefix ) ), 03505 'cl_timestamp=cl_timestamp' ), 03506 array( 03507 'cl_from' => $pageid, 03508 'cl_to' => $catTo ), 03509 __METHOD__ 03510 ); 03511 } 03512 03513 $redirid = $this->getArticleID(); 03514 03515 if ( $protected ) { 03516 # Protect the redirect title as the title used to be... 03517 $dbw->insertSelect( 'page_restrictions', 'page_restrictions', 03518 array( 03519 'pr_page' => $redirid, 03520 'pr_type' => 'pr_type', 03521 'pr_level' => 'pr_level', 03522 'pr_cascade' => 'pr_cascade', 03523 'pr_user' => 'pr_user', 03524 'pr_expiry' => 'pr_expiry' 03525 ), 03526 array( 'pr_page' => $pageid ), 03527 __METHOD__, 03528 array( 'IGNORE' ) 03529 ); 03530 # Update the protection log 03531 $log = new LogPage( 'protect' ); 03532 $comment = wfMsgForContent( 'prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); 03533 if ( $reason ) { 03534 $comment .= wfMsgForContent( 'colon-separator' ) . $reason; 03535 } 03536 // @todo FIXME: $params? 03537 $log->addEntry( 'move_prot', $nt, $comment, array( $this->getPrefixedText() ) ); 03538 } 03539 03540 # Update watchlists 03541 $oldnamespace = $this->getNamespace() & ~1; 03542 $newnamespace = $nt->getNamespace() & ~1; 03543 $oldtitle = $this->getDBkey(); 03544 $newtitle = $nt->getDBkey(); 03545 03546 if ( $oldnamespace != $newnamespace || $oldtitle != $newtitle ) { 03547 WatchedItem::duplicateEntries( $this, $nt ); 03548 } 03549 03550 $dbw->commit(); 03551 03552 wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); 03553 return true; 03554 } 03555 03565 private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) { 03566 global $wgUser, $wgContLang; 03567 03568 if ( $nt->exists() ) { 03569 $moveOverRedirect = true; 03570 $logType = 'move_redir'; 03571 } else { 03572 $moveOverRedirect = false; 03573 $logType = 'move'; 03574 } 03575 03576 $redirectSuppressed = !$createRedirect && $wgUser->isAllowed( 'suppressredirect' ); 03577 03578 $logEntry = new ManualLogEntry( 'move', $logType ); 03579 $logEntry->setPerformer( $wgUser ); 03580 $logEntry->setTarget( $this ); 03581 $logEntry->setComment( $reason ); 03582 $logEntry->setParameters( array( 03583 '4::target' => $nt->getPrefixedText(), 03584 '5::noredir' => $redirectSuppressed ? '1': '0', 03585 ) ); 03586 03587 $formatter = LogFormatter::newFromEntry( $logEntry ); 03588 $formatter->setContext( RequestContext::newExtraneousContext( $this ) ); 03589 $comment = $formatter->getPlainActionText(); 03590 if ( $reason ) { 03591 $comment .= wfMsgForContent( 'colon-separator' ) . $reason; 03592 } 03593 # Truncate for whole multibyte characters. 03594 $comment = $wgContLang->truncate( $comment, 255 ); 03595 03596 $oldid = $this->getArticleID(); 03597 $latest = $this->getLatestRevID(); 03598 03599 $dbw = wfGetDB( DB_MASTER ); 03600 03601 $newpage = WikiPage::factory( $nt ); 03602 03603 if ( $moveOverRedirect ) { 03604 $newid = $nt->getArticleID(); 03605 03606 # Delete the old redirect. We don't save it to history since 03607 # by definition if we've got here it's rather uninteresting. 03608 # We have to remove it so that the next step doesn't trigger 03609 # a conflict on the unique namespace+title index... 03610 $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ ); 03611 03612 $newpage->doDeleteUpdates( $newid ); 03613 } 03614 03615 # Save a null revision in the page's history notifying of the move 03616 $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); 03617 if ( !is_object( $nullRevision ) ) { 03618 throw new MWException( 'No valid null revision produced in ' . __METHOD__ ); 03619 } 03620 $nullRevId = $nullRevision->insertOn( $dbw ); 03621 03622 # Change the name of the target page: 03623 $dbw->update( 'page', 03624 /* SET */ array( 03625 'page_namespace' => $nt->getNamespace(), 03626 'page_title' => $nt->getDBkey(), 03627 ), 03628 /* WHERE */ array( 'page_id' => $oldid ), 03629 __METHOD__ 03630 ); 03631 03632 $this->resetArticleID( 0 ); 03633 $nt->resetArticleID( $oldid ); 03634 03635 $newpage->updateRevisionOn( $dbw, $nullRevision ); 03636 03637 wfRunHooks( 'NewRevisionFromEditComplete', 03638 array( $newpage, $nullRevision, $latest, $wgUser ) ); 03639 03640 $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) ); 03641 03642 if ( !$moveOverRedirect ) { 03643 WikiPage::onArticleCreate( $nt ); 03644 } 03645 03646 # Recreate the redirect, this time in the other direction. 03647 if ( $redirectSuppressed ) { 03648 WikiPage::onArticleDelete( $this ); 03649 } else { 03650 $mwRedir = MagicWord::get( 'redirect' ); 03651 $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; 03652 $redirectArticle = WikiPage::factory( $this ); 03653 $newid = $redirectArticle->insertOn( $dbw ); 03654 if ( $newid ) { // sanity 03655 $redirectRevision = new Revision( array( 03656 'page' => $newid, 03657 'comment' => $comment, 03658 'text' => $redirectText ) ); 03659 $redirectRevision->insertOn( $dbw ); 03660 $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); 03661 03662 wfRunHooks( 'NewRevisionFromEditComplete', 03663 array( $redirectArticle, $redirectRevision, false, $wgUser ) ); 03664 03665 $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) ); 03666 } 03667 } 03668 03669 # Log the move 03670 $logid = $logEntry->insert(); 03671 $logEntry->publish( $logid ); 03672 } 03673 03686 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { 03687 global $wgMaximumMovedPages; 03688 // Check permissions 03689 if ( !$this->userCan( 'move-subpages' ) ) { 03690 return array( 'cant-move-subpages' ); 03691 } 03692 // Do the source and target namespaces support subpages? 03693 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { 03694 return array( 'namespace-nosubpages', 03695 MWNamespace::getCanonicalName( $this->getNamespace() ) ); 03696 } 03697 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) { 03698 return array( 'namespace-nosubpages', 03699 MWNamespace::getCanonicalName( $nt->getNamespace() ) ); 03700 } 03701 03702 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 ); 03703 $retval = array(); 03704 $count = 0; 03705 foreach ( $subpages as $oldSubpage ) { 03706 $count++; 03707 if ( $count > $wgMaximumMovedPages ) { 03708 $retval[$oldSubpage->getPrefixedTitle()] = 03709 array( 'movepage-max-pages', 03710 $wgMaximumMovedPages ); 03711 break; 03712 } 03713 03714 // We don't know whether this function was called before 03715 // or after moving the root page, so check both 03716 // $this and $nt 03717 if ( $oldSubpage->getArticleId() == $this->getArticleId() || 03718 $oldSubpage->getArticleID() == $nt->getArticleId() ) 03719 { 03720 // When moving a page to a subpage of itself, 03721 // don't move it twice 03722 continue; 03723 } 03724 $newPageName = preg_replace( 03725 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#', 03726 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234 03727 $oldSubpage->getDBkey() ); 03728 if ( $oldSubpage->isTalkPage() ) { 03729 $newNs = $nt->getTalkPage()->getNamespace(); 03730 } else { 03731 $newNs = $nt->getSubjectPage()->getNamespace(); 03732 } 03733 # Bug 14385: we need makeTitleSafe because the new page names may 03734 # be longer than 255 characters. 03735 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); 03736 03737 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); 03738 if ( $success === true ) { 03739 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); 03740 } else { 03741 $retval[$oldSubpage->getPrefixedText()] = $success; 03742 } 03743 } 03744 return $retval; 03745 } 03746 03753 public function isSingleRevRedirect() { 03754 $dbw = wfGetDB( DB_MASTER ); 03755 # Is it a redirect? 03756 $row = $dbw->selectRow( 'page', 03757 array( 'page_is_redirect', 'page_latest', 'page_id' ), 03758 $this->pageCond(), 03759 __METHOD__, 03760 array( 'FOR UPDATE' ) 03761 ); 03762 # Cache some fields we may want 03763 $this->mArticleID = $row ? intval( $row->page_id ) : 0; 03764 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false; 03765 $this->mLatestID = $row ? intval( $row->page_latest ) : false; 03766 if ( !$this->mRedirect ) { 03767 return false; 03768 } 03769 # Does the article have a history? 03770 $row = $dbw->selectField( array( 'page', 'revision' ), 03771 'rev_id', 03772 array( 'page_namespace' => $this->getNamespace(), 03773 'page_title' => $this->getDBkey(), 03774 'page_id=rev_page', 03775 'page_latest != rev_id' 03776 ), 03777 __METHOD__, 03778 array( 'FOR UPDATE' ) 03779 ); 03780 # Return true if there was no history 03781 return ( $row === false ); 03782 } 03783 03791 public function isValidMoveTarget( $nt ) { 03792 # Is it an existing file? 03793 if ( $nt->getNamespace() == NS_FILE ) { 03794 $file = wfLocalFile( $nt ); 03795 if ( $file->exists() ) { 03796 wfDebug( __METHOD__ . ": file exists\n" ); 03797 return false; 03798 } 03799 } 03800 # Is it a redirect with no history? 03801 if ( !$nt->isSingleRevRedirect() ) { 03802 wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); 03803 return false; 03804 } 03805 # Get the article text 03806 $rev = Revision::newFromTitle( $nt ); 03807 if( !is_object( $rev ) ){ 03808 return false; 03809 } 03810 $text = $rev->getText(); 03811 # Does the redirect point to the source? 03812 # Or is it a broken self-redirect, usually caused by namespace collisions? 03813 $m = array(); 03814 if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) { 03815 $redirTitle = Title::newFromText( $m[1] ); 03816 if ( !is_object( $redirTitle ) || 03817 ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() && 03818 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) { 03819 wfDebug( __METHOD__ . ": redirect points to other page\n" ); 03820 return false; 03821 } 03822 } else { 03823 # Fail safe 03824 wfDebug( __METHOD__ . ": failsafe\n" ); 03825 return false; 03826 } 03827 return true; 03828 } 03829 03837 public function getParentCategories() { 03838 global $wgContLang; 03839 03840 $data = array(); 03841 03842 $titleKey = $this->getArticleId(); 03843 03844 if ( $titleKey === 0 ) { 03845 return $data; 03846 } 03847 03848 $dbr = wfGetDB( DB_SLAVE ); 03849 03850 $res = $dbr->select( 'categorylinks', '*', 03851 array( 03852 'cl_from' => $titleKey, 03853 ), 03854 __METHOD__, 03855 array() 03856 ); 03857 03858 if ( $dbr->numRows( $res ) > 0 ) { 03859 foreach ( $res as $row ) { 03860 // $data[] = Title::newFromText($wgContLang->getNSText ( NS_CATEGORY ).':'.$row->cl_to); 03861 $data[$wgContLang->getNSText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText(); 03862 } 03863 } 03864 return $data; 03865 } 03866 03873 public function getParentCategoryTree( $children = array() ) { 03874 $stack = array(); 03875 $parents = $this->getParentCategories(); 03876 03877 if ( $parents ) { 03878 foreach ( $parents as $parent => $current ) { 03879 if ( array_key_exists( $parent, $children ) ) { 03880 # Circular reference 03881 $stack[$parent] = array(); 03882 } else { 03883 $nt = Title::newFromText( $parent ); 03884 if ( $nt ) { 03885 $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) ); 03886 } 03887 } 03888 } 03889 } 03890 03891 return $stack; 03892 } 03893 03900 public function pageCond() { 03901 if ( $this->mArticleID > 0 ) { 03902 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs 03903 return array( 'page_id' => $this->mArticleID ); 03904 } else { 03905 return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ); 03906 } 03907 } 03908 03916 public function getPreviousRevisionID( $revId, $flags = 0 ) { 03917 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 03918 return $db->selectField( 'revision', 'rev_id', 03919 array( 03920 'rev_page' => $this->getArticleId( $flags ), 03921 'rev_id < ' . intval( $revId ) 03922 ), 03923 __METHOD__, 03924 array( 'ORDER BY' => 'rev_id DESC' ) 03925 ); 03926 } 03927 03935 public function getNextRevisionID( $revId, $flags = 0 ) { 03936 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 03937 return $db->selectField( 'revision', 'rev_id', 03938 array( 03939 'rev_page' => $this->getArticleId( $flags ), 03940 'rev_id > ' . intval( $revId ) 03941 ), 03942 __METHOD__, 03943 array( 'ORDER BY' => 'rev_id' ) 03944 ); 03945 } 03946 03953 public function getFirstRevision( $flags = 0 ) { 03954 $pageId = $this->getArticleId( $flags ); 03955 if ( $pageId ) { 03956 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); 03957 $row = $db->selectRow( 'revision', '*', 03958 array( 'rev_page' => $pageId ), 03959 __METHOD__, 03960 array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) 03961 ); 03962 if ( $row ) { 03963 return new Revision( $row ); 03964 } 03965 } 03966 return null; 03967 } 03968 03975 public function getEarliestRevTime( $flags = 0 ) { 03976 $rev = $this->getFirstRevision( $flags ); 03977 return $rev ? $rev->getTimestamp() : null; 03978 } 03979 03985 public function isNewPage() { 03986 $dbr = wfGetDB( DB_SLAVE ); 03987 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); 03988 } 03989 03995 public function isBigDeletion() { 03996 global $wgDeleteRevisionsLimit; 03997 03998 if ( !$wgDeleteRevisionsLimit ) { 03999 return false; 04000 } 04001 04002 $revCount = $this->estimateRevisionCount(); 04003 return $revCount > $wgDeleteRevisionsLimit; 04004 } 04005 04011 public function estimateRevisionCount() { 04012 if ( !$this->exists() ) { 04013 return 0; 04014 } 04015 04016 if ( $this->mEstimateRevisions === null ) { 04017 $dbr = wfGetDB( DB_SLAVE ); 04018 $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*', 04019 array( 'rev_page' => $this->getArticleId() ), __METHOD__ ); 04020 } 04021 04022 return $this->mEstimateRevisions; 04023 } 04024 04033 public function countRevisionsBetween( $old, $new ) { 04034 if ( !( $old instanceof Revision ) ) { 04035 $old = Revision::newFromTitle( $this, (int)$old ); 04036 } 04037 if ( !( $new instanceof Revision ) ) { 04038 $new = Revision::newFromTitle( $this, (int)$new ); 04039 } 04040 if ( !$old || !$new ) { 04041 return 0; // nothing to compare 04042 } 04043 $dbr = wfGetDB( DB_SLAVE ); 04044 return (int)$dbr->selectField( 'revision', 'count(*)', 04045 array( 04046 'rev_page' => $this->getArticleId(), 04047 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04048 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04049 ), 04050 __METHOD__ 04051 ); 04052 } 04053 04063 public function countAuthorsBetween( $old, $new, $limit ) { 04064 if ( !( $old instanceof Revision ) ) { 04065 $old = Revision::newFromTitle( $this, (int)$old ); 04066 } 04067 if ( !( $new instanceof Revision ) ) { 04068 $new = Revision::newFromTitle( $this, (int)$new ); 04069 } 04070 if ( !$old || !$new ) { 04071 return 0; // nothing to compare 04072 } 04073 $dbr = wfGetDB( DB_SLAVE ); 04074 $res = $dbr->select( 'revision', 'DISTINCT rev_user_text', 04075 array( 04076 'rev_page' => $this->getArticleID(), 04077 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ), 04078 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) ) 04079 ), __METHOD__, 04080 array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated 04081 ); 04082 return (int)$dbr->numRows( $res ); 04083 } 04084 04091 public function equals( Title $title ) { 04092 // Note: === is necessary for proper matching of number-like titles. 04093 return $this->getInterwiki() === $title->getInterwiki() 04094 && $this->getNamespace() == $title->getNamespace() 04095 && $this->getDBkey() === $title->getDBkey(); 04096 } 04097 04104 public function isSubpageOf( Title $title ) { 04105 return $this->getInterwiki() === $title->getInterwiki() 04106 && $this->getNamespace() == $title->getNamespace() 04107 && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; 04108 } 04109 04119 public function exists() { 04120 return $this->getArticleId() != 0; 04121 } 04122 04139 public function isAlwaysKnown() { 04140 if ( $this->mInterwiki != '' ) { 04141 return true; // any interwiki link might be viewable, for all we know 04142 } 04143 switch( $this->mNamespace ) { 04144 case NS_MEDIA: 04145 case NS_FILE: 04146 // file exists, possibly in a foreign repo 04147 return (bool)wfFindFile( $this ); 04148 case NS_SPECIAL: 04149 // valid special page 04150 return SpecialPageFactory::exists( $this->getDBkey() ); 04151 case NS_MAIN: 04152 // selflink, possibly with fragment 04153 return $this->mDbkeyform == ''; 04154 case NS_MEDIAWIKI: 04155 // known system message 04156 return $this->hasSourceText() !== false; 04157 default: 04158 return false; 04159 } 04160 } 04161 04170 public function isKnown() { 04171 return $this->isAlwaysKnown() || $this->exists(); 04172 } 04173 04179 public function hasSourceText() { 04180 if ( $this->exists() ) { 04181 return true; 04182 } 04183 04184 if ( $this->mNamespace == NS_MEDIAWIKI ) { 04185 // If the page doesn't exist but is a known system message, default 04186 // message content will be displayed, same for language subpages- 04187 // Use always content language to avoid loading hundreds of languages 04188 // to get the link color. 04189 global $wgContLang; 04190 list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04191 $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false ); 04192 return $message->exists(); 04193 } 04194 04195 return false; 04196 } 04197 04203 public function getDefaultMessageText() { 04204 global $wgContLang; 04205 04206 if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case 04207 return false; 04208 } 04209 04210 list( $name, $lang ) = MessageCache::singleton()->figureMessage( $wgContLang->lcfirst( $this->getText() ) ); 04211 $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false ); 04212 04213 if ( $message->exists() ) { 04214 return $message->plain(); 04215 } else { 04216 return false; 04217 } 04218 } 04219 04225 public function invalidateCache() { 04226 if ( wfReadOnly() ) { 04227 return false; 04228 } 04229 $dbw = wfGetDB( DB_MASTER ); 04230 $success = $dbw->update( 04231 'page', 04232 array( 'page_touched' => $dbw->timestamp() ), 04233 $this->pageCond(), 04234 __METHOD__ 04235 ); 04236 HTMLFileCache::clearFileCache( $this ); 04237 return $success; 04238 } 04239 04245 public function touchLinks() { 04246 $u = new HTMLCacheUpdate( $this, 'pagelinks' ); 04247 $u->doUpdate(); 04248 04249 if ( $this->getNamespace() == NS_CATEGORY ) { 04250 $u = new HTMLCacheUpdate( $this, 'categorylinks' ); 04251 $u->doUpdate(); 04252 } 04253 } 04254 04261 public function getTouched( $db = null ) { 04262 $db = isset( $db ) ? $db : wfGetDB( DB_SLAVE ); 04263 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); 04264 return $touched; 04265 } 04266 04273 public function getNotificationTimestamp( $user = null ) { 04274 global $wgUser, $wgShowUpdatedMarker; 04275 // Assume current user if none given 04276 if ( !$user ) { 04277 $user = $wgUser; 04278 } 04279 // Check cache first 04280 $uid = $user->getId(); 04281 // avoid isset here, as it'll return false for null entries 04282 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) { 04283 return $this->mNotificationTimestamp[$uid]; 04284 } 04285 if ( !$uid || !$wgShowUpdatedMarker ) { 04286 return $this->mNotificationTimestamp[$uid] = false; 04287 } 04288 // Don't cache too much! 04289 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) { 04290 $this->mNotificationTimestamp = array(); 04291 } 04292 $dbr = wfGetDB( DB_SLAVE ); 04293 $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', 04294 'wl_notificationtimestamp', 04295 array( 'wl_namespace' => $this->getNamespace(), 04296 'wl_title' => $this->getDBkey(), 04297 'wl_user' => $user->getId() 04298 ), 04299 __METHOD__ 04300 ); 04301 return $this->mNotificationTimestamp[$uid]; 04302 } 04303 04310 public function getNamespaceKey( $prepend = 'nstab-' ) { 04311 global $wgContLang; 04312 // Gets the subject namespace if this title 04313 $namespace = MWNamespace::getSubject( $this->getNamespace() ); 04314 // Checks if cononical namespace name exists for namespace 04315 if ( MWNamespace::exists( $this->getNamespace() ) ) { 04316 // Uses canonical namespace name 04317 $namespaceKey = MWNamespace::getCanonicalName( $namespace ); 04318 } else { 04319 // Uses text of namespace 04320 $namespaceKey = $this->getSubjectNsText(); 04321 } 04322 // Makes namespace key lowercase 04323 $namespaceKey = $wgContLang->lc( $namespaceKey ); 04324 // Uses main 04325 if ( $namespaceKey == '' ) { 04326 $namespaceKey = 'main'; 04327 } 04328 // Changes file to image for backwards compatibility 04329 if ( $namespaceKey == 'file' ) { 04330 $namespaceKey = 'image'; 04331 } 04332 return $prepend . $namespaceKey; 04333 } 04334 04341 public function getRedirectsHere( $ns = null ) { 04342 $redirs = array(); 04343 04344 $dbr = wfGetDB( DB_SLAVE ); 04345 $where = array( 04346 'rd_namespace' => $this->getNamespace(), 04347 'rd_title' => $this->getDBkey(), 04348 'rd_from = page_id' 04349 ); 04350 if ( !is_null( $ns ) ) { 04351 $where['page_namespace'] = $ns; 04352 } 04353 04354 $res = $dbr->select( 04355 array( 'redirect', 'page' ), 04356 array( 'page_namespace', 'page_title' ), 04357 $where, 04358 __METHOD__ 04359 ); 04360 04361 foreach ( $res as $row ) { 04362 $redirs[] = self::newFromRow( $row ); 04363 } 04364 return $redirs; 04365 } 04366 04372 public function isValidRedirectTarget() { 04373 global $wgInvalidRedirectTargets; 04374 04375 // invalid redirect targets are stored in a global array, but explicity disallow Userlogout here 04376 if ( $this->isSpecial( 'Userlogout' ) ) { 04377 return false; 04378 } 04379 04380 foreach ( $wgInvalidRedirectTargets as $target ) { 04381 if ( $this->isSpecial( $target ) ) { 04382 return false; 04383 } 04384 } 04385 04386 return true; 04387 } 04388 04394 function getBacklinkCache() { 04395 if ( is_null( $this->mBacklinkCache ) ) { 04396 $this->mBacklinkCache = new BacklinkCache( $this ); 04397 } 04398 return $this->mBacklinkCache; 04399 } 04400 04406 public function canUseNoindex() { 04407 global $wgContentNamespaces, $wgExemptFromUserRobotsControl; 04408 04409 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl ) 04410 ? $wgContentNamespaces 04411 : $wgExemptFromUserRobotsControl; 04412 04413 return !in_array( $this->mNamespace, $bannedNamespaces ); 04414 04415 } 04416 04427 public function getCategorySortkey( $prefix = '' ) { 04428 $unprefixed = $this->getText(); 04429 04430 // Anything that uses this hook should only depend 04431 // on the Title object passed in, and should probably 04432 // tell the users to run updateCollations.php --force 04433 // in order to re-sort existing category relations. 04434 wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) ); 04435 if ( $prefix !== '' ) { 04436 # Separate with a line feed, so the unprefixed part is only used as 04437 # a tiebreaker when two pages have the exact same prefix. 04438 # In UCA, tab is the only character that can sort above LF 04439 # so we strip both of them from the original prefix. 04440 $prefix = strtr( $prefix, "\n\t", ' ' ); 04441 return "$prefix\n$unprefixed"; 04442 } 04443 return $unprefixed; 04444 } 04445 04454 public function getPageLanguage() { 04455 global $wgLang; 04456 if ( $this->isSpecialPage() ) { 04457 // special pages are in the user language 04458 return $wgLang; 04459 } elseif ( $this->isCssOrJsPage() ) { 04460 // css/js should always be LTR and is, in fact, English 04461 return wfGetLangObj( 'en' ); 04462 } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) { 04463 // Parse mediawiki messages with correct target language 04464 list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() ); 04465 return wfGetLangObj( $lang ); 04466 } 04467 global $wgContLang; 04468 // If nothing special, it should be in the wiki content language 04469 $pageLang = $wgContLang; 04470 // Hook at the end because we don't want to override the above stuff 04471 wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) ); 04472 return wfGetLangObj( $pageLang ); 04473 } 04474 }