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