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