MediaWiki  REL1_23
WikiPage.php
Go to the documentation of this file.
00001 <?php
00026 interface Page {
00027 }
00028 
00037 class WikiPage implements Page, IDBAccessObject {
00038     // Constants for $mDataLoadedFrom and related
00039 
00043     public $mTitle = null;
00044 
00048     public $mDataLoaded = false;         // !< Boolean
00049     public $mIsRedirect = false;         // !< Boolean
00050     public $mLatest = false;             // !< Integer (false means "not loaded")
00054     public $mPreparedEdit = false;
00055 
00059     protected $mId = null;
00060 
00064     protected $mDataLoadedFrom = self::READ_NONE;
00065 
00069     protected $mRedirectTarget = null;
00070 
00074     protected $mLastRevision = null;
00075 
00079     protected $mTimestamp = '';
00080 
00084     protected $mTouched = '19700101000000';
00085 
00089     protected $mLinksUpdated = '19700101000000';
00090 
00094     protected $mCounter = null;
00095 
00100     public function __construct( Title $title ) {
00101         $this->mTitle = $title;
00102     }
00103 
00112     public static function factory( Title $title ) {
00113         $ns = $title->getNamespace();
00114 
00115         if ( $ns == NS_MEDIA ) {
00116             throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
00117         } elseif ( $ns < 0 ) {
00118             throw new MWException( "Invalid or virtual namespace $ns given." );
00119         }
00120 
00121         switch ( $ns ) {
00122             case NS_FILE:
00123                 $page = new WikiFilePage( $title );
00124                 break;
00125             case NS_CATEGORY:
00126                 $page = new WikiCategoryPage( $title );
00127                 break;
00128             default:
00129                 $page = new WikiPage( $title );
00130         }
00131 
00132         return $page;
00133     }
00134 
00145     public static function newFromID( $id, $from = 'fromdb' ) {
00146         // page id's are never 0 or negative, see bug 61166
00147         if ( $id < 1 ) {
00148             return null;
00149         }
00150 
00151         $from = self::convertSelectType( $from );
00152         $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
00153         $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
00154         if ( !$row ) {
00155             return null;
00156         }
00157         return self::newFromRow( $row, $from );
00158     }
00159 
00172     public static function newFromRow( $row, $from = 'fromdb' ) {
00173         $page = self::factory( Title::newFromRow( $row ) );
00174         $page->loadFromRow( $row, $from );
00175         return $page;
00176     }
00177 
00184     private static function convertSelectType( $type ) {
00185         switch ( $type ) {
00186         case 'fromdb':
00187             return self::READ_NORMAL;
00188         case 'fromdbmaster':
00189             return self::READ_LATEST;
00190         case 'forupdate':
00191             return self::READ_LOCKING;
00192         default:
00193             // It may already be an integer or whatever else
00194             return $type;
00195         }
00196     }
00197 
00208     public function getActionOverrides() {
00209         $content_handler = $this->getContentHandler();
00210         return $content_handler->getActionOverrides();
00211     }
00212 
00222     public function getContentHandler() {
00223         return ContentHandler::getForModelID( $this->getContentModel() );
00224     }
00225 
00230     public function getTitle() {
00231         return $this->mTitle;
00232     }
00233 
00238     public function clear() {
00239         $this->mDataLoaded = false;
00240         $this->mDataLoadedFrom = self::READ_NONE;
00241 
00242         $this->clearCacheFields();
00243     }
00244 
00249     protected function clearCacheFields() {
00250         $this->mId = null;
00251         $this->mCounter = null;
00252         $this->mRedirectTarget = null; // Title object if set
00253         $this->mLastRevision = null; // Latest revision
00254         $this->mTouched = '19700101000000';
00255         $this->mLinksUpdated = '19700101000000';
00256         $this->mTimestamp = '';
00257         $this->mIsRedirect = false;
00258         $this->mLatest = false;
00259         // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
00260         // the requested rev ID and content against the cached one for equality. For most
00261         // content types, the output should not change during the lifetime of this cache.
00262         // Clearing it can cause extra parses on edit for no reason.
00263     }
00264 
00270     public function clearPreparedEdit() {
00271         $this->mPreparedEdit = false;
00272     }
00273 
00280     public static function selectFields() {
00281         global $wgContentHandlerUseDB;
00282 
00283         $fields = array(
00284             'page_id',
00285             'page_namespace',
00286             'page_title',
00287             'page_restrictions',
00288             'page_counter',
00289             'page_is_redirect',
00290             'page_is_new',
00291             'page_random',
00292             'page_touched',
00293             'page_links_updated',
00294             'page_latest',
00295             'page_len',
00296         );
00297 
00298         if ( $wgContentHandlerUseDB ) {
00299             $fields[] = 'page_content_model';
00300         }
00301 
00302         return $fields;
00303     }
00304 
00312     protected function pageData( $dbr, $conditions, $options = array() ) {
00313         $fields = self::selectFields();
00314 
00315         wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) );
00316 
00317         $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
00318 
00319         wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) );
00320 
00321         return $row;
00322     }
00323 
00333     public function pageDataFromTitle( $dbr, $title, $options = array() ) {
00334         return $this->pageData( $dbr, array(
00335             'page_namespace' => $title->getNamespace(),
00336             'page_title' => $title->getDBkey() ), $options );
00337     }
00338 
00347     public function pageDataFromId( $dbr, $id, $options = array() ) {
00348         return $this->pageData( $dbr, array( 'page_id' => $id ), $options );
00349     }
00350 
00363     public function loadPageData( $from = 'fromdb' ) {
00364         $from = self::convertSelectType( $from );
00365         if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
00366             // We already have the data from the correct location, no need to load it twice.
00367             return;
00368         }
00369 
00370         if ( $from === self::READ_LOCKING ) {
00371             $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) );
00372         } elseif ( $from === self::READ_LATEST ) {
00373             $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
00374         } elseif ( $from === self::READ_NORMAL ) {
00375             $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle );
00376             // Use a "last rev inserted" timestamp key to diminish the issue of slave lag.
00377             // Note that DB also stores the master position in the session and checks it.
00378             $touched = $this->getCachedLastEditTime();
00379             if ( $touched ) { // key set
00380                 if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
00381                     $from = self::READ_LATEST;
00382                     $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
00383                 }
00384             }
00385         } else {
00386             // No idea from where the caller got this data, assume slave database.
00387             $data = $from;
00388             $from = self::READ_NORMAL;
00389         }
00390 
00391         $this->loadFromRow( $data, $from );
00392     }
00393 
00406     public function loadFromRow( $data, $from ) {
00407         $lc = LinkCache::singleton();
00408         $lc->clearLink( $this->mTitle );
00409 
00410         if ( $data ) {
00411             $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
00412 
00413             $this->mTitle->loadFromRow( $data );
00414 
00415             // Old-fashioned restrictions
00416             $this->mTitle->loadRestrictions( $data->page_restrictions );
00417 
00418             $this->mId = intval( $data->page_id );
00419             $this->mCounter = intval( $data->page_counter );
00420             $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
00421             $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
00422             $this->mIsRedirect = intval( $data->page_is_redirect );
00423             $this->mLatest = intval( $data->page_latest );
00424             // Bug 37225: $latest may no longer match the cached latest Revision object.
00425             // Double-check the ID of any cached latest Revision object for consistency.
00426             if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
00427                 $this->mLastRevision = null;
00428                 $this->mTimestamp = '';
00429             }
00430         } else {
00431             $lc->addBadLinkObj( $this->mTitle );
00432 
00433             $this->mTitle->loadFromRow( false );
00434 
00435             $this->clearCacheFields();
00436 
00437             $this->mId = 0;
00438         }
00439 
00440         $this->mDataLoaded = true;
00441         $this->mDataLoadedFrom = self::convertSelectType( $from );
00442     }
00443 
00447     public function getId() {
00448         if ( !$this->mDataLoaded ) {
00449             $this->loadPageData();
00450         }
00451         return $this->mId;
00452     }
00453 
00457     public function exists() {
00458         if ( !$this->mDataLoaded ) {
00459             $this->loadPageData();
00460         }
00461         return $this->mId > 0;
00462     }
00463 
00472     public function hasViewableContent() {
00473         return $this->exists() || $this->mTitle->isAlwaysKnown();
00474     }
00475 
00479     public function getCount() {
00480         if ( !$this->mDataLoaded ) {
00481             $this->loadPageData();
00482         }
00483 
00484         return $this->mCounter;
00485     }
00486 
00492     public function isRedirect() {
00493         $content = $this->getContent();
00494         if ( !$content ) {
00495             return false;
00496         }
00497 
00498         return $content->isRedirect();
00499     }
00500 
00511     public function getContentModel() {
00512         if ( $this->exists() ) {
00513             // look at the revision's actual content model
00514             $rev = $this->getRevision();
00515 
00516             if ( $rev !== null ) {
00517                 return $rev->getContentModel();
00518             } else {
00519                 $title = $this->mTitle->getPrefixedDBkey();
00520                 wfWarn( "Page $title exists but has no (visible) revisions!" );
00521             }
00522         }
00523 
00524         // use the default model for this page
00525         return $this->mTitle->getContentModel();
00526     }
00527 
00532     public function checkTouched() {
00533         if ( !$this->mDataLoaded ) {
00534             $this->loadPageData();
00535         }
00536         return !$this->mIsRedirect;
00537     }
00538 
00543     public function getTouched() {
00544         if ( !$this->mDataLoaded ) {
00545             $this->loadPageData();
00546         }
00547         return $this->mTouched;
00548     }
00549 
00554     public function getLinksTimestamp() {
00555         if ( !$this->mDataLoaded ) {
00556             $this->loadPageData();
00557         }
00558         return $this->mLinksUpdated;
00559     }
00560 
00565     public function getLatest() {
00566         if ( !$this->mDataLoaded ) {
00567             $this->loadPageData();
00568         }
00569         return (int)$this->mLatest;
00570     }
00571 
00576     public function getOldestRevision() {
00577         wfProfileIn( __METHOD__ );
00578 
00579         // Try using the slave database first, then try the master
00580         $continue = 2;
00581         $db = wfGetDB( DB_SLAVE );
00582         $revSelectFields = Revision::selectFields();
00583 
00584         $row = null;
00585         while ( $continue ) {
00586             $row = $db->selectRow(
00587                 array( 'page', 'revision' ),
00588                 $revSelectFields,
00589                 array(
00590                     'page_namespace' => $this->mTitle->getNamespace(),
00591                     'page_title' => $this->mTitle->getDBkey(),
00592                     'rev_page = page_id'
00593                 ),
00594                 __METHOD__,
00595                 array(
00596                     'ORDER BY' => 'rev_timestamp ASC'
00597                 )
00598             );
00599 
00600             if ( $row ) {
00601                 $continue = 0;
00602             } else {
00603                 $db = wfGetDB( DB_MASTER );
00604                 $continue--;
00605             }
00606         }
00607 
00608         wfProfileOut( __METHOD__ );
00609         return $row ? Revision::newFromRow( $row ) : null;
00610     }
00611 
00616     protected function loadLastEdit() {
00617         if ( $this->mLastRevision !== null ) {
00618             return; // already loaded
00619         }
00620 
00621         $latest = $this->getLatest();
00622         if ( !$latest ) {
00623             return; // page doesn't exist or is missing page_latest info
00624         }
00625 
00626         // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the
00627         // latest changes committed. This is true even within REPEATABLE-READ transactions, where
00628         // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to
00629         // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row
00630         // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT.
00631         // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read.
00632         $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0;
00633         $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
00634         if ( $revision ) { // sanity
00635             $this->setLastEdit( $revision );
00636         }
00637     }
00638 
00642     protected function setLastEdit( Revision $revision ) {
00643         $this->mLastRevision = $revision;
00644         $this->mTimestamp = $revision->getTimestamp();
00645     }
00646 
00651     public function getRevision() {
00652         $this->loadLastEdit();
00653         if ( $this->mLastRevision ) {
00654             return $this->mLastRevision;
00655         }
00656         return null;
00657     }
00658 
00672     public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
00673         $this->loadLastEdit();
00674         if ( $this->mLastRevision ) {
00675             return $this->mLastRevision->getContent( $audience, $user );
00676         }
00677         return null;
00678     }
00679 
00692     public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { // @todo deprecated, replace usage!
00693         ContentHandler::deprecated( __METHOD__, '1.21' );
00694 
00695         $this->loadLastEdit();
00696         if ( $this->mLastRevision ) {
00697             return $this->mLastRevision->getText( $audience, $user );
00698         }
00699         return false;
00700     }
00701 
00708     public function getRawText() {
00709         ContentHandler::deprecated( __METHOD__, '1.21' );
00710 
00711         return $this->getText( Revision::RAW );
00712     }
00713 
00717     public function getTimestamp() {
00718         // Check if the field has been filled by WikiPage::setTimestamp()
00719         if ( !$this->mTimestamp ) {
00720             $this->loadLastEdit();
00721         }
00722 
00723         return wfTimestamp( TS_MW, $this->mTimestamp );
00724     }
00725 
00731     public function setTimestamp( $ts ) {
00732         $this->mTimestamp = wfTimestamp( TS_MW, $ts );
00733     }
00734 
00744     public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
00745         $this->loadLastEdit();
00746         if ( $this->mLastRevision ) {
00747             return $this->mLastRevision->getUser( $audience, $user );
00748         } else {
00749             return -1;
00750         }
00751     }
00752 
00763     public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
00764         $revision = $this->getOldestRevision();
00765         if ( $revision ) {
00766             $userName = $revision->getUserText( $audience, $user );
00767             return User::newFromName( $userName, false );
00768         } else {
00769             return null;
00770         }
00771     }
00772 
00782     public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
00783         $this->loadLastEdit();
00784         if ( $this->mLastRevision ) {
00785             return $this->mLastRevision->getUserText( $audience, $user );
00786         } else {
00787             return '';
00788         }
00789     }
00790 
00800     public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
00801         $this->loadLastEdit();
00802         if ( $this->mLastRevision ) {
00803             return $this->mLastRevision->getComment( $audience, $user );
00804         } else {
00805             return '';
00806         }
00807     }
00808 
00814     public function getMinorEdit() {
00815         $this->loadLastEdit();
00816         if ( $this->mLastRevision ) {
00817             return $this->mLastRevision->isMinor();
00818         } else {
00819             return false;
00820         }
00821     }
00822 
00828     protected function getCachedLastEditTime() {
00829         global $wgMemc;
00830         $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
00831         return $wgMemc->get( $key );
00832     }
00833 
00840     public function setCachedLastEditTime( $timestamp ) {
00841         global $wgMemc;
00842         $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
00843         $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60 * 15 );
00844     }
00845 
00854     public function isCountable( $editInfo = false ) {
00855         global $wgArticleCountMethod;
00856 
00857         if ( !$this->mTitle->isContentPage() ) {
00858             return false;
00859         }
00860 
00861         if ( $editInfo ) {
00862             $content = $editInfo->pstContent;
00863         } else {
00864             $content = $this->getContent();
00865         }
00866 
00867         if ( !$content || $content->isRedirect() ) {
00868             return false;
00869         }
00870 
00871         $hasLinks = null;
00872 
00873         if ( $wgArticleCountMethod === 'link' ) {
00874             // nasty special case to avoid re-parsing to detect links
00875 
00876             if ( $editInfo ) {
00877                 // ParserOutput::getLinks() is a 2D array of page links, so
00878                 // to be really correct we would need to recurse in the array
00879                 // but the main array should only have items in it if there are
00880                 // links.
00881                 $hasLinks = (bool)count( $editInfo->output->getLinks() );
00882             } else {
00883                 $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
00884                     array( 'pl_from' => $this->getId() ), __METHOD__ );
00885             }
00886         }
00887 
00888         return $content->isCountable( $hasLinks );
00889     }
00890 
00898     public function getRedirectTarget() {
00899         if ( !$this->mTitle->isRedirect() ) {
00900             return null;
00901         }
00902 
00903         if ( $this->mRedirectTarget !== null ) {
00904             return $this->mRedirectTarget;
00905         }
00906 
00907         // Query the redirect table
00908         $dbr = wfGetDB( DB_SLAVE );
00909         $row = $dbr->selectRow( 'redirect',
00910             array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ),
00911             array( 'rd_from' => $this->getId() ),
00912             __METHOD__
00913         );
00914 
00915         // rd_fragment and rd_interwiki were added later, populate them if empty
00916         if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
00917             $this->mRedirectTarget = Title::makeTitle(
00918                 $row->rd_namespace, $row->rd_title,
00919                 $row->rd_fragment, $row->rd_interwiki );
00920             return $this->mRedirectTarget;
00921         }
00922 
00923         // This page doesn't have an entry in the redirect table
00924         $this->mRedirectTarget = $this->insertRedirect();
00925         return $this->mRedirectTarget;
00926     }
00927 
00934     public function insertRedirect() {
00935         // recurse through to only get the final target
00936         $content = $this->getContent();
00937         $retval = $content ? $content->getUltimateRedirectTarget() : null;
00938         if ( !$retval ) {
00939             return null;
00940         }
00941         $this->insertRedirectEntry( $retval );
00942         return $retval;
00943     }
00944 
00950     public function insertRedirectEntry( $rt ) {
00951         $dbw = wfGetDB( DB_MASTER );
00952         $dbw->replace( 'redirect', array( 'rd_from' ),
00953             array(
00954                 'rd_from' => $this->getId(),
00955                 'rd_namespace' => $rt->getNamespace(),
00956                 'rd_title' => $rt->getDBkey(),
00957                 'rd_fragment' => $rt->getFragment(),
00958                 'rd_interwiki' => $rt->getInterwiki(),
00959             ),
00960             __METHOD__
00961         );
00962     }
00963 
00969     public function followRedirect() {
00970         return $this->getRedirectURL( $this->getRedirectTarget() );
00971     }
00972 
00980     public function getRedirectURL( $rt ) {
00981         if ( !$rt ) {
00982             return false;
00983         }
00984 
00985         if ( $rt->isExternal() ) {
00986             if ( $rt->isLocal() ) {
00987                 // Offsite wikis need an HTTP redirect.
00988                 //
00989                 // This can be hard to reverse and may produce loops,
00990                 // so they may be disabled in the site configuration.
00991                 $source = $this->mTitle->getFullURL( 'redirect=no' );
00992                 return $rt->getFullURL( array( 'rdfrom' => $source ) );
00993             } else {
00994                 // External pages pages without "local" bit set are not valid
00995                 // redirect targets
00996                 return false;
00997             }
00998         }
00999 
01000         if ( $rt->isSpecialPage() ) {
01001             // Gotta handle redirects to special pages differently:
01002             // Fill the HTTP response "Location" header and ignore
01003             // the rest of the page we're on.
01004             //
01005             // Some pages are not valid targets
01006             if ( $rt->isValidRedirectTarget() ) {
01007                 return $rt->getFullURL();
01008             } else {
01009                 return false;
01010             }
01011         }
01012 
01013         return $rt;
01014     }
01015 
01021     public function getContributors() {
01022         // @todo FIXME: This is expensive; cache this info somewhere.
01023 
01024         $dbr = wfGetDB( DB_SLAVE );
01025 
01026         if ( $dbr->implicitGroupby() ) {
01027             $realNameField = 'user_real_name';
01028         } else {
01029             $realNameField = 'MIN(user_real_name) AS user_real_name';
01030         }
01031 
01032         $tables = array( 'revision', 'user' );
01033 
01034         $fields = array(
01035             'user_id' => 'rev_user',
01036             'user_name' => 'rev_user_text',
01037             $realNameField,
01038             'timestamp' => 'MAX(rev_timestamp)',
01039         );
01040 
01041         $conds = array( 'rev_page' => $this->getId() );
01042 
01043         // The user who made the top revision gets credited as "this page was last edited by
01044         // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
01045         $user = $this->getUser();
01046         if ( $user ) {
01047             $conds[] = "rev_user != $user";
01048         } else {
01049             $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
01050         }
01051 
01052         $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden?
01053 
01054         $jconds = array(
01055             'user' => array( 'LEFT JOIN', 'rev_user = user_id' ),
01056         );
01057 
01058         $options = array(
01059             'GROUP BY' => array( 'rev_user', 'rev_user_text' ),
01060             'ORDER BY' => 'timestamp DESC',
01061         );
01062 
01063         $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
01064         return new UserArrayFromResult( $res );
01065     }
01066 
01073     public function getLastNAuthors( $num, $revLatest = 0 ) {
01074         wfProfileIn( __METHOD__ );
01075         // First try the slave
01076         // If that doesn't have the latest revision, try the master
01077         $continue = 2;
01078         $db = wfGetDB( DB_SLAVE );
01079 
01080         do {
01081             $res = $db->select( array( 'page', 'revision' ),
01082                 array( 'rev_id', 'rev_user_text' ),
01083                 array(
01084                     'page_namespace' => $this->mTitle->getNamespace(),
01085                     'page_title' => $this->mTitle->getDBkey(),
01086                     'rev_page = page_id'
01087                 ), __METHOD__,
01088                 array(
01089                     'ORDER BY' => 'rev_timestamp DESC',
01090                     'LIMIT' => $num
01091                 )
01092             );
01093 
01094             if ( !$res ) {
01095                 wfProfileOut( __METHOD__ );
01096                 return array();
01097             }
01098 
01099             $row = $db->fetchObject( $res );
01100 
01101             if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
01102                 $db = wfGetDB( DB_MASTER );
01103                 $continue--;
01104             } else {
01105                 $continue = 0;
01106             }
01107         } while ( $continue );
01108 
01109         $authors = array( $row->rev_user_text );
01110 
01111         foreach ( $res as $row ) {
01112             $authors[] = $row->rev_user_text;
01113         }
01114 
01115         wfProfileOut( __METHOD__ );
01116         return $authors;
01117     }
01118 
01126     public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) {
01127         global $wgEnableParserCache;
01128 
01129         return $wgEnableParserCache
01130             && $parserOptions->getStubThreshold() == 0
01131             && $this->exists()
01132             && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
01133             && $this->getContentHandler()->isParserCacheSupported();
01134     }
01135 
01147     public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
01148         wfProfileIn( __METHOD__ );
01149 
01150         $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid );
01151         wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
01152         if ( $parserOptions->getStubThreshold() ) {
01153             wfIncrStats( 'pcache_miss_stub' );
01154         }
01155 
01156         if ( $useParserCache ) {
01157             $parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
01158             if ( $parserOutput !== false ) {
01159                 wfProfileOut( __METHOD__ );
01160                 return $parserOutput;
01161             }
01162         }
01163 
01164         if ( $oldid === null || $oldid === 0 ) {
01165             $oldid = $this->getLatest();
01166         }
01167 
01168         $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
01169         $pool->execute();
01170 
01171         wfProfileOut( __METHOD__ );
01172 
01173         return $pool->getParserOutput();
01174     }
01175 
01181     public function doViewUpdates( User $user, $oldid = 0 ) {
01182         global $wgDisableCounters;
01183         if ( wfReadOnly() ) {
01184             return;
01185         }
01186 
01187         // Don't update page view counters on views from bot users (bug 14044)
01188         if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->exists() ) {
01189             DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) );
01190             DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) );
01191         }
01192 
01193         // Update newtalk / watchlist notification status
01194         $user->clearNotification( $this->mTitle, $oldid );
01195     }
01196 
01201     public function doPurge() {
01202         global $wgUseSquid;
01203 
01204         if ( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
01205             return false;
01206         }
01207 
01208         // Invalidate the cache
01209         $this->mTitle->invalidateCache();
01210 
01211         if ( $wgUseSquid ) {
01212             // Commit the transaction before the purge is sent
01213             $dbw = wfGetDB( DB_MASTER );
01214             $dbw->commit( __METHOD__ );
01215 
01216             // Send purge
01217             $update = SquidUpdate::newSimplePurge( $this->mTitle );
01218             $update->doUpdate();
01219         }
01220 
01221         if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
01222             // @todo move this logic to MessageCache
01223 
01224             if ( $this->exists() ) {
01225                 // NOTE: use transclusion text for messages.
01226                 //       This is consistent with  MessageCache::getMsgFromNamespace()
01227 
01228                 $content = $this->getContent();
01229                 $text = $content === null ? null : $content->getWikitextForTransclusion();
01230 
01231                 if ( $text === null ) {
01232                     $text = false;
01233                 }
01234             } else {
01235                 $text = false;
01236             }
01237 
01238             MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
01239         }
01240         return true;
01241     }
01242 
01253     public function insertOn( $dbw ) {
01254         wfProfileIn( __METHOD__ );
01255 
01256         $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' );
01257         $dbw->insert( 'page', array(
01258             'page_id'           => $page_id,
01259             'page_namespace'    => $this->mTitle->getNamespace(),
01260             'page_title'        => $this->mTitle->getDBkey(),
01261             'page_counter'      => 0,
01262             'page_restrictions' => '',
01263             'page_is_redirect'  => 0, // Will set this shortly...
01264             'page_is_new'       => 1,
01265             'page_random'       => wfRandom(),
01266             'page_touched'      => $dbw->timestamp(),
01267             'page_latest'       => 0, // Fill this in shortly...
01268             'page_len'          => 0, // Fill this in shortly...
01269         ), __METHOD__, 'IGNORE' );
01270 
01271         $affected = $dbw->affectedRows();
01272 
01273         if ( $affected ) {
01274             $newid = $dbw->insertId();
01275             $this->mId = $newid;
01276             $this->mTitle->resetArticleID( $newid );
01277         }
01278         wfProfileOut( __METHOD__ );
01279 
01280         return $affected ? $newid : false;
01281     }
01282 
01298     public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
01299         global $wgContentHandlerUseDB;
01300 
01301         wfProfileIn( __METHOD__ );
01302 
01303         $content = $revision->getContent();
01304         $len = $content ? $content->getSize() : 0;
01305         $rt = $content ? $content->getUltimateRedirectTarget() : null;
01306 
01307         $conditions = array( 'page_id' => $this->getId() );
01308 
01309         if ( !is_null( $lastRevision ) ) {
01310             // An extra check against threads stepping on each other
01311             $conditions['page_latest'] = $lastRevision;
01312         }
01313 
01314         $now = wfTimestampNow();
01315         $row = array( /* SET */
01316             'page_latest'      => $revision->getId(),
01317             'page_touched'     => $dbw->timestamp( $now ),
01318             'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
01319             'page_is_redirect' => $rt !== null ? 1 : 0,
01320             'page_len'         => $len,
01321         );
01322 
01323         if ( $wgContentHandlerUseDB ) {
01324             $row['page_content_model'] = $revision->getContentModel();
01325         }
01326 
01327         $dbw->update( 'page',
01328             $row,
01329             $conditions,
01330             __METHOD__ );
01331 
01332         $result = $dbw->affectedRows() > 0;
01333         if ( $result ) {
01334             $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
01335             $this->setLastEdit( $revision );
01336             $this->setCachedLastEditTime( $now );
01337             $this->mLatest = $revision->getId();
01338             $this->mIsRedirect = (bool)$rt;
01339             // Update the LinkCache.
01340             LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect,
01341                                                     $this->mLatest, $revision->getContentModel() );
01342         }
01343 
01344         wfProfileOut( __METHOD__ );
01345         return $result;
01346     }
01347 
01359     public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
01360         // Always update redirects (target link might have changed)
01361         // Update/Insert if we don't know if the last revision was a redirect or not
01362         // Delete if changing from redirect to non-redirect
01363         $isRedirect = !is_null( $redirectTitle );
01364 
01365         if ( !$isRedirect && $lastRevIsRedirect === false ) {
01366             return true;
01367         }
01368 
01369         wfProfileIn( __METHOD__ );
01370         if ( $isRedirect ) {
01371             $this->insertRedirectEntry( $redirectTitle );
01372         } else {
01373             // This is not a redirect, remove row from redirect table
01374             $where = array( 'rd_from' => $this->getId() );
01375             $dbw->delete( 'redirect', $where, __METHOD__ );
01376         }
01377 
01378         if ( $this->getTitle()->getNamespace() == NS_FILE ) {
01379             RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
01380         }
01381         wfProfileOut( __METHOD__ );
01382 
01383         return ( $dbw->affectedRows() != 0 );
01384     }
01385 
01394     public function updateIfNewerOn( $dbw, $revision ) {
01395         wfProfileIn( __METHOD__ );
01396 
01397         $row = $dbw->selectRow(
01398             array( 'revision', 'page' ),
01399             array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ),
01400             array(
01401                 'page_id' => $this->getId(),
01402                 'page_latest=rev_id' ),
01403             __METHOD__ );
01404 
01405         if ( $row ) {
01406             if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
01407                 wfProfileOut( __METHOD__ );
01408                 return false;
01409             }
01410             $prev = $row->rev_id;
01411             $lastRevIsRedirect = (bool)$row->page_is_redirect;
01412         } else {
01413             // No or missing previous revision; mark the page as new
01414             $prev = 0;
01415             $lastRevIsRedirect = null;
01416         }
01417 
01418         $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
01419 
01420         wfProfileOut( __METHOD__ );
01421         return $ret;
01422     }
01423 
01434     public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
01435         $handler = $undo->getContentHandler();
01436         return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
01437     }
01438 
01448     public function getUndoText( Revision $undo, Revision $undoafter = null ) {
01449         ContentHandler::deprecated( __METHOD__, '1.21' );
01450 
01451         $this->loadLastEdit();
01452 
01453         if ( $this->mLastRevision ) {
01454             if ( is_null( $undoafter ) ) {
01455                 $undoafter = $undo->getPrevious();
01456             }
01457 
01458             $handler = $this->getContentHandler();
01459             $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
01460 
01461             if ( !$undone ) {
01462                 return false;
01463             } else {
01464                 return ContentHandler::getContentText( $undone );
01465             }
01466         }
01467 
01468         return false;
01469     }
01470 
01482     public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
01483         ContentHandler::deprecated( __METHOD__, '1.21' );
01484 
01485         if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent!
01486             // Whole-page edit; let the whole text through
01487             return $text;
01488         }
01489 
01490         if ( !$this->supportsSections() ) {
01491             throw new MWException( "sections not supported for content model " .
01492                 $this->getContentHandler()->getModelID() );
01493         }
01494 
01495         // could even make section title, but that's not required.
01496         $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() );
01497 
01498         $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle,
01499             $edittime );
01500 
01501         return ContentHandler::getContentText( $newContent );
01502     }
01503 
01512     public function supportsSections() {
01513         return $this->getContentHandler()->supportsSections();
01514     }
01515 
01527     public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '',
01528         $edittime = null ) {
01529         wfProfileIn( __METHOD__ );
01530 
01531         if ( strval( $section ) == '' ) {
01532             // Whole-page edit; let the whole text through
01533             $newContent = $sectionContent;
01534         } else {
01535             if ( !$this->supportsSections() ) {
01536                 wfProfileOut( __METHOD__ );
01537                 throw new MWException( "sections not supported for content model " .
01538                     $this->getContentHandler()->getModelID() );
01539             }
01540 
01541             // Bug 30711: always use current version when adding a new section
01542             if ( is_null( $edittime ) || $section == 'new' ) {
01543                 $oldContent = $this->getContent();
01544             } else {
01545                 $dbw = wfGetDB( DB_MASTER );
01546                 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
01547 
01548                 if ( !$rev ) {
01549                     wfDebug( "WikiPage::replaceSection asked for bogus section (page: " .
01550                         $this->getId() . "; section: $section; edittime: $edittime)\n" );
01551                     wfProfileOut( __METHOD__ );
01552                     return null;
01553                 }
01554 
01555                 $oldContent = $rev->getContent();
01556             }
01557 
01558             if ( ! $oldContent ) {
01559                 wfDebug( __METHOD__ . ": no page text\n" );
01560                 wfProfileOut( __METHOD__ );
01561                 return null;
01562             }
01563 
01564             // FIXME: $oldContent might be null?
01565             $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
01566         }
01567 
01568         wfProfileOut( __METHOD__ );
01569         return $newContent;
01570     }
01571 
01577     public function checkFlags( $flags ) {
01578         if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
01579             if ( $this->exists() ) {
01580                 $flags |= EDIT_UPDATE;
01581             } else {
01582                 $flags |= EDIT_NEW;
01583             }
01584         }
01585 
01586         return $flags;
01587     }
01588 
01638     public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
01639         ContentHandler::deprecated( __METHOD__, '1.21' );
01640 
01641         $content = ContentHandler::makeContent( $text, $this->getTitle() );
01642 
01643         return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
01644     }
01645 
01694     public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
01695         User $user = null, $serialisation_format = null
01696     ) {
01697         global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
01698 
01699         // Low-level sanity check
01700         if ( $this->mTitle->getText() === '' ) {
01701             throw new MWException( 'Something is trying to edit an article with an empty title' );
01702         }
01703 
01704         wfProfileIn( __METHOD__ );
01705 
01706         if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) {
01707             wfProfileOut( __METHOD__ );
01708             return Status::newFatal( 'content-not-allowed-here',
01709                 ContentHandler::getLocalizedName( $content->getModel() ),
01710                 $this->getTitle()->getPrefixedText() );
01711         }
01712 
01713         $user = is_null( $user ) ? $wgUser : $user;
01714         $status = Status::newGood( array() );
01715 
01716         // Load the data from the master database if needed.
01717         // The caller may already loaded it from the master or even loaded it using
01718         // SELECT FOR UPDATE, so do not override that using clear().
01719         $this->loadPageData( 'fromdbmaster' );
01720 
01721         $flags = $this->checkFlags( $flags );
01722 
01723         // handle hook
01724         $hook_args = array( &$this, &$user, &$content, &$summary,
01725                             $flags & EDIT_MINOR, null, null, &$flags, &$status );
01726 
01727         if ( !wfRunHooks( 'PageContentSave', $hook_args )
01728             || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
01729 
01730             wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
01731 
01732             if ( $status->isOK() ) {
01733                 $status->fatal( 'edit-hook-aborted' );
01734             }
01735 
01736             wfProfileOut( __METHOD__ );
01737             return $status;
01738         }
01739 
01740         // Silently ignore EDIT_MINOR if not allowed
01741         $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
01742         $bot = $flags & EDIT_FORCE_BOT;
01743 
01744         $old_content = $this->getContent( Revision::RAW ); // current revision's content
01745 
01746         $oldsize = $old_content ? $old_content->getSize() : 0;
01747         $oldid = $this->getLatest();
01748         $oldIsRedirect = $this->isRedirect();
01749         $oldcountable = $this->isCountable();
01750 
01751         $handler = $content->getContentHandler();
01752 
01753         // Provide autosummaries if one is not provided and autosummaries are enabled.
01754         if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
01755             if ( !$old_content ) {
01756                 $old_content = null;
01757             }
01758             $summary = $handler->getAutosummary( $old_content, $content, $flags );
01759         }
01760 
01761         $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
01762         $serialized = $editInfo->pst;
01763 
01767         $content = $editInfo->pstContent;
01768         $newsize = $content->getSize();
01769 
01770         $dbw = wfGetDB( DB_MASTER );
01771         $now = wfTimestampNow();
01772         $this->mTimestamp = $now;
01773 
01774         if ( $flags & EDIT_UPDATE ) {
01775             // Update article, but only if changed.
01776             $status->value['new'] = false;
01777 
01778             if ( !$oldid ) {
01779                 // Article gone missing
01780                 wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" );
01781                 $status->fatal( 'edit-gone-missing' );
01782 
01783                 wfProfileOut( __METHOD__ );
01784                 return $status;
01785             } elseif ( !$old_content ) {
01786                 // Sanity check for bug 37225
01787                 wfProfileOut( __METHOD__ );
01788                 throw new MWException( "Could not find text for current revision {$oldid}." );
01789             }
01790 
01791             $revision = new Revision( array(
01792                 'page'       => $this->getId(),
01793                 'title'      => $this->getTitle(), // for determining the default content model
01794                 'comment'    => $summary,
01795                 'minor_edit' => $isminor,
01796                 'text'       => $serialized,
01797                 'len'        => $newsize,
01798                 'parent_id'  => $oldid,
01799                 'user'       => $user->getId(),
01800                 'user_text'  => $user->getName(),
01801                 'timestamp'  => $now,
01802                 'content_model' => $content->getModel(),
01803                 'content_format' => $serialisation_format,
01804             ) ); // XXX: pass content object?!
01805 
01806             $changed = !$content->equals( $old_content );
01807 
01808             if ( $changed ) {
01809                 if ( !$content->isValid() ) {
01810                     wfProfileOut( __METHOD__ );
01811                     throw new MWException( "New content failed validity check!" );
01812                 }
01813 
01814                 $dbw->begin( __METHOD__ );
01815                 try {
01816 
01817                     $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
01818                     $status->merge( $prepStatus );
01819 
01820                     if ( !$status->isOK() ) {
01821                         $dbw->rollback( __METHOD__ );
01822 
01823                         wfProfileOut( __METHOD__ );
01824                         return $status;
01825                     }
01826                     $revisionId = $revision->insertOn( $dbw );
01827 
01828                     // Update page
01829                     //
01830                     // Note that we use $this->mLatest instead of fetching a value from the master DB
01831                     // during the course of this function. This makes sure that EditPage can detect
01832                     // edit conflicts reliably, either by $ok here, or by $article->getTimestamp()
01833                     // before this function is called. A previous function used a separate query, this
01834                     // creates a window where concurrent edits can cause an ignored edit conflict.
01835                     $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect );
01836 
01837                     if ( !$ok ) {
01838                         // Belated edit conflict! Run away!!
01839                         $status->fatal( 'edit-conflict' );
01840 
01841                         $dbw->rollback( __METHOD__ );
01842 
01843                         wfProfileOut( __METHOD__ );
01844                         return $status;
01845                     }
01846 
01847                     wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
01848                     // Update recentchanges
01849                     if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
01850                         // Mark as patrolled if the user can do so
01851                         $patrolled = $wgUseRCPatrol && !count(
01852                         $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
01853                         // Add RC row to the DB
01854                         $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
01855                             $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
01856                             $revisionId, $patrolled
01857                         );
01858 
01859                         // Log auto-patrolled edits
01860                         if ( $patrolled ) {
01861                             PatrolLog::record( $rc, true, $user );
01862                         }
01863                     }
01864                     $user->incEditCount();
01865                 } catch ( MWException $e ) {
01866                     $dbw->rollback( __METHOD__ );
01867                     // Question: Would it perhaps be better if this method turned all
01868                     // exceptions into $status's?
01869                     throw $e;
01870                 }
01871                 $dbw->commit( __METHOD__ );
01872             } else {
01873                 // Bug 32948: revision ID must be set to page {{REVISIONID}} and
01874                 // related variables correctly
01875                 $revision->setId( $this->getLatest() );
01876             }
01877 
01878             // Update links tables, site stats, etc.
01879             $this->doEditUpdates(
01880                 $revision,
01881                 $user,
01882                 array(
01883                     'changed' => $changed,
01884                     'oldcountable' => $oldcountable
01885                 )
01886             );
01887 
01888             if ( !$changed ) {
01889                 $status->warning( 'edit-no-change' );
01890                 $revision = null;
01891                 // Update page_touched, this is usually implicit in the page update
01892                 // Other cache updates are done in onArticleEdit()
01893                 $this->mTitle->invalidateCache();
01894             }
01895         } else {
01896             // Create new article
01897             $status->value['new'] = true;
01898 
01899             $dbw->begin( __METHOD__ );
01900             try {
01901 
01902                 $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
01903                 $status->merge( $prepStatus );
01904 
01905                 if ( !$status->isOK() ) {
01906                     $dbw->rollback( __METHOD__ );
01907 
01908                     wfProfileOut( __METHOD__ );
01909                     return $status;
01910                 }
01911 
01912                 $status->merge( $prepStatus );
01913 
01914                 // Add the page record; stake our claim on this title!
01915                 // This will return false if the article already exists
01916                 $newid = $this->insertOn( $dbw );
01917 
01918                 if ( $newid === false ) {
01919                     $dbw->rollback( __METHOD__ );
01920                     $status->fatal( 'edit-already-exists' );
01921 
01922                     wfProfileOut( __METHOD__ );
01923                     return $status;
01924                 }
01925 
01926                 // Save the revision text...
01927                 $revision = new Revision( array(
01928                     'page'       => $newid,
01929                     'title'      => $this->getTitle(), // for determining the default content model
01930                     'comment'    => $summary,
01931                     'minor_edit' => $isminor,
01932                     'text'       => $serialized,
01933                     'len'        => $newsize,
01934                     'user'       => $user->getId(),
01935                     'user_text'  => $user->getName(),
01936                     'timestamp'  => $now,
01937                     'content_model' => $content->getModel(),
01938                     'content_format' => $serialisation_format,
01939                 ) );
01940                 $revisionId = $revision->insertOn( $dbw );
01941 
01942                 // Bug 37225: use accessor to get the text as Revision may trim it
01943                 $content = $revision->getContent(); // sanity; get normalized version
01944 
01945                 if ( $content ) {
01946                     $newsize = $content->getSize();
01947                 }
01948 
01949                 // Update the page record with revision data
01950                 $this->updateRevisionOn( $dbw, $revision, 0 );
01951 
01952                 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
01953 
01954                 // Update recentchanges
01955                 if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
01956                     // Mark as patrolled if the user can do so
01957                     $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
01958                         $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
01959                     // Add RC row to the DB
01960                     $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
01961                         '', $newsize, $revisionId, $patrolled );
01962 
01963                     // Log auto-patrolled edits
01964                     if ( $patrolled ) {
01965                         PatrolLog::record( $rc, true, $user );
01966                     }
01967                 }
01968                 $user->incEditCount();
01969 
01970             } catch ( MWException $e ) {
01971                 $dbw->rollback( __METHOD__ );
01972                 throw $e;
01973             }
01974             $dbw->commit( __METHOD__ );
01975 
01976             // Update links, etc.
01977             $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
01978 
01979             $hook_args = array( &$this, &$user, $content, $summary,
01980                                 $flags & EDIT_MINOR, null, null, &$flags, $revision );
01981 
01982             ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
01983             wfRunHooks( 'PageContentInsertComplete', $hook_args );
01984         }
01985 
01986         // Do updates right now unless deferral was requested
01987         if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
01988             DeferredUpdates::doUpdates();
01989         }
01990 
01991         // Return the new revision (or null) to the caller
01992         $status->value['revision'] = $revision;
01993 
01994         $hook_args = array( &$this, &$user, $content, $summary,
01995                             $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
01996 
01997         ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
01998         wfRunHooks( 'PageContentSaveComplete', $hook_args );
01999 
02000         // Promote user to any groups they meet the criteria for
02001         $user->addAutopromoteOnceGroups( 'onEdit' );
02002 
02003         wfProfileOut( __METHOD__ );
02004         return $status;
02005     }
02006 
02021     public function makeParserOptions( $context ) {
02022         $options = $this->getContentHandler()->makeParserOptions( $context );
02023 
02024         if ( $this->getTitle()->isConversionTable() ) {
02025             // @todo ConversionTable should become a separate content model, so we don't need special cases like this one.
02026             $options->disableContentConversion();
02027         }
02028 
02029         return $options;
02030     }
02031 
02038     public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
02039         ContentHandler::deprecated( __METHOD__, '1.21' );
02040         $content = ContentHandler::makeContent( $text, $this->getTitle() );
02041         return $this->prepareContentForEdit( $content, $revid, $user );
02042     }
02043 
02057     public function prepareContentForEdit( Content $content, $revid = null, User $user = null,
02058         $serialization_format = null
02059     ) {
02060         global $wgContLang, $wgUser;
02061         $user = is_null( $user ) ? $wgUser : $user;
02062         //XXX: check $user->getId() here???
02063 
02064         // Use a sane default for $serialization_format, see bug 57026
02065         if ( $serialization_format === null ) {
02066             $serialization_format = $content->getContentHandler()->getDefaultFormat();
02067         }
02068 
02069         if ( $this->mPreparedEdit
02070             && $this->mPreparedEdit->newContent
02071             && $this->mPreparedEdit->newContent->equals( $content )
02072             && $this->mPreparedEdit->revid == $revid
02073             && $this->mPreparedEdit->format == $serialization_format
02074             // XXX: also check $user here?
02075         ) {
02076             // Already prepared
02077             return $this->mPreparedEdit;
02078         }
02079 
02080         $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
02081         wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
02082 
02083         $edit = (object)array();
02084         $edit->revid = $revid;
02085         $edit->timestamp = wfTimestampNow();
02086 
02087         $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null;
02088 
02089         $edit->format = $serialization_format;
02090         $edit->popts = $this->makeParserOptions( 'canonical' );
02091         $edit->output = $edit->pstContent ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) : null;
02092 
02093         $edit->newContent = $content;
02094         $edit->oldContent = $this->getContent( Revision::RAW );
02095 
02096         // NOTE: B/C for hooks! don't use these fields!
02097         $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : '';
02098         $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
02099         $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : '';
02100 
02101         $this->mPreparedEdit = $edit;
02102         return $edit;
02103     }
02104 
02121     public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
02122         global $wgEnableParserCache;
02123 
02124         wfProfileIn( __METHOD__ );
02125 
02126         $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
02127         $content = $revision->getContent();
02128 
02129         // Parse the text
02130         // Be careful not to do pre-save transform twice: $text is usually
02131         // already pre-save transformed once.
02132         if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
02133             wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
02134             $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
02135         } else {
02136             wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
02137             $editInfo = $this->mPreparedEdit;
02138         }
02139 
02140         // Save it to the parser cache
02141         if ( $wgEnableParserCache ) {
02142             $parserCache = ParserCache::singleton();
02143             $parserCache->save(
02144                 $editInfo->output, $this, $editInfo->popts, $editInfo->timestamp, $editInfo->revid
02145             );
02146         }
02147 
02148         // Update the links tables and other secondary data
02149         if ( $content ) {
02150             $recursive = $options['changed']; // bug 50785
02151             $updates = $content->getSecondaryDataUpdates(
02152                 $this->getTitle(), null, $recursive, $editInfo->output );
02153             DataUpdate::runUpdates( $updates );
02154         }
02155 
02156         wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
02157 
02158         if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
02159             if ( 0 == mt_rand( 0, 99 ) ) {
02160                 // Flush old entries from the `recentchanges` table; we do this on
02161                 // random requests so as to avoid an increase in writes for no good reason
02162                 RecentChange::purgeExpiredChanges();
02163             }
02164         }
02165 
02166         if ( !$this->exists() ) {
02167             wfProfileOut( __METHOD__ );
02168             return;
02169         }
02170 
02171         $id = $this->getId();
02172         $title = $this->mTitle->getPrefixedDBkey();
02173         $shortTitle = $this->mTitle->getDBkey();
02174 
02175         if ( !$options['changed'] ) {
02176             $good = 0;
02177             $total = 0;
02178         } elseif ( $options['created'] ) {
02179             $good = (int)$this->isCountable( $editInfo );
02180             $total = 1;
02181         } elseif ( $options['oldcountable'] !== null ) {
02182             $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
02183             $total = 0;
02184         } else {
02185             $good = 0;
02186             $total = 0;
02187         }
02188 
02189         DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
02190         DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
02191 
02192         // If this is another user's talk page, update newtalk.
02193         // Don't do this if $options['changed'] = false (null-edits) nor if
02194         // it's a minor edit and the user doesn't want notifications for those.
02195         if ( $options['changed']
02196             && $this->mTitle->getNamespace() == NS_USER_TALK
02197             && $shortTitle != $user->getTitleKey()
02198             && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
02199         ) {
02200             $recipient = User::newFromName( $shortTitle, false );
02201             if ( !$recipient ) {
02202                 wfDebug( __METHOD__ . ": invalid username\n" );
02203             } else {
02204                 // Allow extensions to prevent user notification when a new message is added to their talk page
02205                 if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) {
02206                     if ( User::isIP( $shortTitle ) ) {
02207                         // An anonymous user
02208                         $recipient->setNewtalk( true, $revision );
02209                     } elseif ( $recipient->isLoggedIn() ) {
02210                         $recipient->setNewtalk( true, $revision );
02211                     } else {
02212                         wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
02213                     }
02214                 }
02215             }
02216         }
02217 
02218         if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
02219             // XXX: could skip pseudo-messages like js/css here, based on content model.
02220             $msgtext = $content ? $content->getWikitextForTransclusion() : null;
02221             if ( $msgtext === false || $msgtext === null ) {
02222                 $msgtext = '';
02223             }
02224 
02225             MessageCache::singleton()->replace( $shortTitle, $msgtext );
02226         }
02227 
02228         if ( $options['created'] ) {
02229             self::onArticleCreate( $this->mTitle );
02230         } else {
02231             self::onArticleEdit( $this->mTitle );
02232         }
02233 
02234         wfProfileOut( __METHOD__ );
02235     }
02236 
02249     public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
02250         ContentHandler::deprecated( __METHOD__, "1.21" );
02251 
02252         $content = ContentHandler::makeContent( $text, $this->getTitle() );
02253         $this->doQuickEditContent( $content, $user, $comment, $minor );
02254     }
02255 
02267     public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false,
02268         $serialisation_format = null
02269     ) {
02270         wfProfileIn( __METHOD__ );
02271 
02272         $serialized = $content->serialize( $serialisation_format );
02273 
02274         $dbw = wfGetDB( DB_MASTER );
02275         $revision = new Revision( array(
02276             'title'      => $this->getTitle(), // for determining the default content model
02277             'page'       => $this->getId(),
02278             'text'       => $serialized,
02279             'length'     => $content->getSize(),
02280             'comment'    => $comment,
02281             'minor_edit' => $minor ? 1 : 0,
02282         ) ); // XXX: set the content object?
02283         $revision->insertOn( $dbw );
02284         $this->updateRevisionOn( $dbw, $revision );
02285 
02286         wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
02287 
02288         wfProfileOut( __METHOD__ );
02289     }
02290 
02302     public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) {
02303         global $wgCascadingRestrictionLevels, $wgContLang;
02304 
02305         if ( wfReadOnly() ) {
02306             return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
02307         }
02308 
02309         $this->loadPageData( 'fromdbmaster' );
02310         $restrictionTypes = $this->mTitle->getRestrictionTypes();
02311         $id = $this->getId();
02312 
02313         if ( !$cascade ) {
02314             $cascade = false;
02315         }
02316 
02317         // Take this opportunity to purge out expired restrictions
02318         Title::purgeExpiredRestrictions();
02319 
02320         // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
02321         // we expect a single selection, but the schema allows otherwise.
02322         $isProtected = false;
02323         $protect = false;
02324         $changed = false;
02325 
02326         $dbw = wfGetDB( DB_MASTER );
02327 
02328         foreach ( $restrictionTypes as $action ) {
02329             if ( !isset( $expiry[$action] ) ) {
02330                 $expiry[$action] = $dbw->getInfinity();
02331             }
02332             if ( !isset( $limit[$action] ) ) {
02333                 $limit[$action] = '';
02334             } elseif ( $limit[$action] != '' ) {
02335                 $protect = true;
02336             }
02337 
02338             // Get current restrictions on $action
02339             $current = implode( '', $this->mTitle->getRestrictions( $action ) );
02340             if ( $current != '' ) {
02341                 $isProtected = true;
02342             }
02343 
02344             if ( $limit[$action] != $current ) {
02345                 $changed = true;
02346             } elseif ( $limit[$action] != '' ) {
02347                 // Only check expiry change if the action is actually being
02348                 // protected, since expiry does nothing on an not-protected
02349                 // action.
02350                 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
02351                     $changed = true;
02352                 }
02353             }
02354         }
02355 
02356         if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
02357             $changed = true;
02358         }
02359 
02360         // If nothing has changed, do nothing
02361         if ( !$changed ) {
02362             return Status::newGood();
02363         }
02364 
02365         if ( !$protect ) { // No protection at all means unprotection
02366             $revCommentMsg = 'unprotectedarticle';
02367             $logAction = 'unprotect';
02368         } elseif ( $isProtected ) {
02369             $revCommentMsg = 'modifiedarticleprotection';
02370             $logAction = 'modify';
02371         } else {
02372             $revCommentMsg = 'protectedarticle';
02373             $logAction = 'protect';
02374         }
02375 
02376         // Truncate for whole multibyte characters
02377         $reason = $wgContLang->truncate( $reason, 255 );
02378 
02379         $logRelationsValues = array();
02380         $logRelationsField = null;
02381 
02382         if ( $id ) { // Protection of existing page
02383             if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
02384                 return Status::newGood();
02385             }
02386 
02387             // Only certain restrictions can cascade...
02388             $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' );
02389             foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
02390                 $editrestriction[$key] = 'editprotected'; // backwards compatibility
02391             }
02392             foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
02393                 $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
02394             }
02395 
02396             $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
02397             foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
02398                 $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
02399             }
02400             foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
02401                 $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
02402             }
02403 
02404             // The schema allows multiple restrictions
02405             if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
02406                 $cascade = false;
02407             }
02408 
02409             // insert null revision to identify the page protection change as edit summary
02410             $latest = $this->getLatest();
02411             $nullRevision = $this->insertProtectNullRevision( $revCommentMsg, $limit, $expiry, $cascade, $reason );
02412             if ( $nullRevision === null ) {
02413                 return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
02414             }
02415 
02416             $logRelationsField = 'pr_id';
02417 
02418             // Update restrictions table
02419             foreach ( $limit as $action => $restrictions ) {
02420                 $dbw->delete(
02421                     'page_restrictions',
02422                     array(
02423                         'pr_page' => $id,
02424                         'pr_type' => $action
02425                     ),
02426                     __METHOD__
02427                 );
02428                 if ( $restrictions != '' ) {
02429                     $dbw->insert(
02430                         'page_restrictions',
02431                         array(
02432                             'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
02433                             'pr_page' => $id,
02434                             'pr_type' => $action,
02435                             'pr_level' => $restrictions,
02436                             'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
02437                             'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
02438                         ),
02439                         __METHOD__
02440                     );
02441                     $logRelationsValues[] = $dbw->insertId();
02442                 }
02443             }
02444 
02445             // Clear out legacy restriction fields
02446             $dbw->update(
02447                 'page',
02448                 array( 'page_restrictions' => '' ),
02449                 array( 'page_id' => $id ),
02450                 __METHOD__
02451             );
02452 
02453             wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) );
02454             wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
02455         } else { // Protection of non-existing page (also known as "title protection")
02456             // Cascade protection is meaningless in this case
02457             $cascade = false;
02458 
02459             if ( $limit['create'] != '' ) {
02460                 $dbw->replace( 'protected_titles',
02461                     array( array( 'pt_namespace', 'pt_title' ) ),
02462                     array(
02463                         'pt_namespace' => $this->mTitle->getNamespace(),
02464                         'pt_title' => $this->mTitle->getDBkey(),
02465                         'pt_create_perm' => $limit['create'],
02466                         'pt_timestamp' => $dbw->timestamp(),
02467                         'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
02468                         'pt_user' => $user->getId(),
02469                         'pt_reason' => $reason,
02470                     ), __METHOD__
02471                 );
02472             } else {
02473                 $dbw->delete( 'protected_titles',
02474                     array(
02475                         'pt_namespace' => $this->mTitle->getNamespace(),
02476                         'pt_title' => $this->mTitle->getDBkey()
02477                     ), __METHOD__
02478                 );
02479             }
02480         }
02481 
02482         $this->mTitle->flushRestrictions();
02483         InfoAction::invalidateCache( $this->mTitle );
02484 
02485         if ( $logAction == 'unprotect' ) {
02486             $params = array();
02487         } else {
02488             $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
02489             $params = array( $protectDescriptionLog, $cascade ? 'cascade' : '' );
02490         }
02491 
02492         // Update the protection log
02493         $log = new LogPage( 'protect' );
02494         $logId = $log->addEntry( $logAction, $this->mTitle, $reason, $params, $user );
02495         if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
02496             $log->addRelations( $logRelationsField, $logRelationsValues, $logId );
02497         }
02498 
02499         return Status::newGood();
02500     }
02501 
02512     public function insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason ) {
02513         global $wgContLang;
02514         $dbw = wfGetDB( DB_MASTER );
02515 
02516         // Prepare a null revision to be added to the history
02517         $editComment = $wgContLang->ucfirst(
02518             wfMessage(
02519                 $revCommentMsg,
02520                 $this->mTitle->getPrefixedText()
02521             )->inContentLanguage()->text()
02522         );
02523         if ( $reason ) {
02524             $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
02525         }
02526         $protectDescription = $this->protectDescription( $limit, $expiry );
02527         if ( $protectDescription ) {
02528             $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
02529             $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )->inContentLanguage()->text();
02530         }
02531         if ( $cascade ) {
02532             $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
02533             $editComment .= wfMessage( 'brackets' )->params(
02534                 wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
02535             )->inContentLanguage()->text();
02536         }
02537 
02538         $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true );
02539         if ( $nullRev ) {
02540             $nullRev->insertOn( $dbw );
02541 
02542             // Update page record and touch page
02543             $oldLatest = $nullRev->getParentId();
02544             $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
02545         }
02546 
02547         return $nullRev;
02548     }
02549 
02554     protected function formatExpiry( $expiry ) {
02555         global $wgContLang;
02556         $dbr = wfGetDB( DB_SLAVE );
02557 
02558         $encodedExpiry = $dbr->encodeExpiry( $expiry );
02559         if ( $encodedExpiry != 'infinity' ) {
02560             return wfMessage(
02561                 'protect-expiring',
02562                 $wgContLang->timeanddate( $expiry, false, false ),
02563                 $wgContLang->date( $expiry, false, false ),
02564                 $wgContLang->time( $expiry, false, false )
02565             )->inContentLanguage()->text();
02566         } else {
02567             return wfMessage( 'protect-expiry-indefinite' )
02568                 ->inContentLanguage()->text();
02569         }
02570     }
02571 
02579     public function protectDescription( array $limit, array $expiry ) {
02580         $protectDescription = '';
02581 
02582         foreach ( array_filter( $limit ) as $action => $restrictions ) {
02583             # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
02584             # All possible message keys are listed here for easier grepping:
02585             # * restriction-create
02586             # * restriction-edit
02587             # * restriction-move
02588             # * restriction-upload
02589             $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
02590             # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
02591             # with '' filtered out. All possible message keys are listed below:
02592             # * protect-level-autoconfirmed
02593             # * protect-level-sysop
02594             $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text();
02595 
02596             $expiryText = $this->formatExpiry( $expiry[$action] );
02597 
02598             if ( $protectDescription !== '' ) {
02599                 $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
02600             }
02601             $protectDescription .= wfMessage( 'protect-summary-desc' )
02602                 ->params( $actionText, $restrictionsText, $expiryText )
02603                 ->inContentLanguage()->text();
02604         }
02605 
02606         return $protectDescription;
02607     }
02608 
02620     public function protectDescriptionLog( array $limit, array $expiry ) {
02621         global $wgContLang;
02622 
02623         $protectDescriptionLog = '';
02624 
02625         foreach ( array_filter( $limit ) as $action => $restrictions ) {
02626             $expiryText = $this->formatExpiry( $expiry[$action] );
02627             $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)";
02628         }
02629 
02630         return trim( $protectDescriptionLog );
02631     }
02632 
02642     protected static function flattenRestrictions( $limit ) {
02643         if ( !is_array( $limit ) ) {
02644             throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' );
02645         }
02646 
02647         $bits = array();
02648         ksort( $limit );
02649 
02650         foreach ( array_filter( $limit ) as $action => $restrictions ) {
02651             $bits[] = "$action=$restrictions";
02652         }
02653 
02654         return implode( ':', $bits );
02655     }
02656 
02673     public function doDeleteArticle(
02674         $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
02675     ) {
02676         $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user );
02677         return $status->isGood();
02678     }
02679 
02697     public function doDeleteArticleReal(
02698         $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
02699     ) {
02700         global $wgUser, $wgContentHandlerUseDB;
02701 
02702         wfDebug( __METHOD__ . "\n" );
02703 
02704         $status = Status::newGood();
02705 
02706         if ( $this->mTitle->getDBkey() === '' ) {
02707             $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
02708             return $status;
02709         }
02710 
02711         $user = is_null( $user ) ? $wgUser : $user;
02712         if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) {
02713             if ( $status->isOK() ) {
02714                 // Hook aborted but didn't set a fatal status
02715                 $status->fatal( 'delete-hook-aborted' );
02716             }
02717             return $status;
02718         }
02719 
02720         if ( $id == 0 ) {
02721             $this->loadPageData( 'forupdate' );
02722             $id = $this->getID();
02723             if ( $id == 0 ) {
02724                 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
02725                 return $status;
02726             }
02727         }
02728 
02729         // Bitfields to further suppress the content
02730         if ( $suppress ) {
02731             $bitfield = 0;
02732             // This should be 15...
02733             $bitfield |= Revision::DELETED_TEXT;
02734             $bitfield |= Revision::DELETED_COMMENT;
02735             $bitfield |= Revision::DELETED_USER;
02736             $bitfield |= Revision::DELETED_RESTRICTED;
02737         } else {
02738             $bitfield = 'rev_deleted';
02739         }
02740 
02741         // we need to remember the old content so we can use it to generate all deletion updates.
02742         $content = $this->getContent( Revision::RAW );
02743 
02744         $dbw = wfGetDB( DB_MASTER );
02745         $dbw->begin( __METHOD__ );
02746         // For now, shunt the revision data into the archive table.
02747         // Text is *not* removed from the text table; bulk storage
02748         // is left intact to avoid breaking block-compression or
02749         // immutable storage schemes.
02750         //
02751         // For backwards compatibility, note that some older archive
02752         // table entries will have ar_text and ar_flags fields still.
02753         //
02754         // In the future, we may keep revisions and mark them with
02755         // the rev_deleted field, which is reserved for this purpose.
02756 
02757         $row = array(
02758             'ar_namespace'  => 'page_namespace',
02759             'ar_title'      => 'page_title',
02760             'ar_comment'    => 'rev_comment',
02761             'ar_user'       => 'rev_user',
02762             'ar_user_text'  => 'rev_user_text',
02763             'ar_timestamp'  => 'rev_timestamp',
02764             'ar_minor_edit' => 'rev_minor_edit',
02765             'ar_rev_id'     => 'rev_id',
02766             'ar_parent_id'  => 'rev_parent_id',
02767             'ar_text_id'    => 'rev_text_id',
02768             'ar_text'       => '\'\'', // Be explicit to appease
02769             'ar_flags'      => '\'\'', // MySQL's "strict mode"...
02770             'ar_len'        => 'rev_len',
02771             'ar_page_id'    => 'page_id',
02772             'ar_deleted'    => $bitfield,
02773             'ar_sha1'       => 'rev_sha1',
02774         );
02775 
02776         if ( $wgContentHandlerUseDB ) {
02777             $row['ar_content_model'] = 'rev_content_model';
02778             $row['ar_content_format'] = 'rev_content_format';
02779         }
02780 
02781         $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
02782             $row,
02783             array(
02784                 'page_id' => $id,
02785                 'page_id = rev_page'
02786             ), __METHOD__
02787         );
02788 
02789         // Now that it's safely backed up, delete it
02790         $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
02791         $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy
02792 
02793         if ( !$ok ) {
02794             $dbw->rollback( __METHOD__ );
02795             $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
02796             return $status;
02797         }
02798 
02799         if ( !$dbw->cascadingDeletes() ) {
02800             $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
02801         }
02802 
02803         $this->doDeleteUpdates( $id, $content );
02804 
02805         // Log the deletion, if the page was suppressed, log it at Oversight instead
02806         $logtype = $suppress ? 'suppress' : 'delete';
02807 
02808         $logEntry = new ManualLogEntry( $logtype, 'delete' );
02809         $logEntry->setPerformer( $user );
02810         $logEntry->setTarget( $this->mTitle );
02811         $logEntry->setComment( $reason );
02812         $logid = $logEntry->insert();
02813         $logEntry->publish( $logid );
02814 
02815         if ( $commit ) {
02816             $dbw->commit( __METHOD__ );
02817         }
02818 
02819         wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) );
02820         $status->value = $logid;
02821         return $status;
02822     }
02823 
02831     public function doDeleteUpdates( $id, Content $content = null ) {
02832         // update site status
02833         DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
02834 
02835         // remove secondary indexes, etc
02836         $updates = $this->getDeletionUpdates( $content );
02837         DataUpdate::runUpdates( $updates );
02838 
02839         // Reparse any pages transcluding this page
02840         LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
02841 
02842         // Reparse any pages including this image
02843         if ( $this->mTitle->getNamespace() == NS_FILE ) {
02844             LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
02845         }
02846 
02847         // Clear caches
02848         WikiPage::onArticleDelete( $this->mTitle );
02849 
02850         // Reset this object and the Title object
02851         $this->loadFromRow( false, self::READ_LATEST );
02852 
02853         // Search engine
02854         DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
02855     }
02856 
02881     public function doRollback(
02882         $fromP, $summary, $token, $bot, &$resultDetails, User $user
02883     ) {
02884         $resultDetails = null;
02885 
02886         // Check permissions
02887         $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
02888         $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
02889         $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
02890 
02891         if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
02892             $errors[] = array( 'sessionfailure' );
02893         }
02894 
02895         if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
02896             $errors[] = array( 'actionthrottledtext' );
02897         }
02898 
02899         // If there were errors, bail out now
02900         if ( !empty( $errors ) ) {
02901             return $errors;
02902         }
02903 
02904         return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user );
02905     }
02906 
02923     public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) {
02924         global $wgUseRCPatrol, $wgContLang;
02925 
02926         $dbw = wfGetDB( DB_MASTER );
02927 
02928         if ( wfReadOnly() ) {
02929             return array( array( 'readonlytext' ) );
02930         }
02931 
02932         // Get the last editor
02933         $current = $this->getRevision();
02934         if ( is_null( $current ) ) {
02935             // Something wrong... no page?
02936             return array( array( 'notanarticle' ) );
02937         }
02938 
02939         $from = str_replace( '_', ' ', $fromP );
02940         // User name given should match up with the top revision.
02941         // If the user was deleted then $from should be empty.
02942         if ( $from != $current->getUserText() ) {
02943             $resultDetails = array( 'current' => $current );
02944             return array( array( 'alreadyrolled',
02945                 htmlspecialchars( $this->mTitle->getPrefixedText() ),
02946                 htmlspecialchars( $fromP ),
02947                 htmlspecialchars( $current->getUserText() )
02948             ) );
02949         }
02950 
02951         // Get the last edit not by this guy...
02952         // Note: these may not be public values
02953         $user = intval( $current->getRawUser() );
02954         $user_text = $dbw->addQuotes( $current->getRawUserText() );
02955         $s = $dbw->selectRow( 'revision',
02956             array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
02957             array( 'rev_page' => $current->getPage(),
02958                 "rev_user != {$user} OR rev_user_text != {$user_text}"
02959             ), __METHOD__,
02960             array( 'USE INDEX' => 'page_timestamp',
02961                 'ORDER BY' => 'rev_timestamp DESC' )
02962             );
02963         if ( $s === false ) {
02964             // No one else ever edited this page
02965             return array( array( 'cantrollback' ) );
02966         } elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) {
02967             // Only admins can see this text
02968             return array( array( 'notvisiblerev' ) );
02969         }
02970 
02971         // Set patrolling and bot flag on the edits, which gets rollbacked.
02972         // This is done before the rollback edit to have patrolling also on failure (bug 62157).
02973         $set = array();
02974         if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
02975             // Mark all reverted edits as bot
02976             $set['rc_bot'] = 1;
02977         }
02978 
02979         if ( $wgUseRCPatrol ) {
02980             // Mark all reverted edits as patrolled
02981             $set['rc_patrolled'] = 1;
02982         }
02983 
02984         if ( count( $set ) ) {
02985             $dbw->update( 'recentchanges', $set,
02986                 array( /* WHERE */
02987                     'rc_cur_id' => $current->getPage(),
02988                     'rc_user_text' => $current->getUserText(),
02989                     'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
02990                 ), __METHOD__
02991             );
02992         }
02993 
02994         // Generate the edit summary if necessary
02995         $target = Revision::newFromId( $s->rev_id );
02996         if ( empty( $summary ) ) {
02997             if ( $from == '' ) { // no public user name
02998                 $summary = wfMessage( 'revertpage-nouser' );
02999             } else {
03000                 $summary = wfMessage( 'revertpage' );
03001             }
03002         }
03003 
03004         // Allow the custom summary to use the same args as the default message
03005         $args = array(
03006             $target->getUserText(), $from, $s->rev_id,
03007             $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
03008             $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
03009         );
03010         if ( $summary instanceof Message ) {
03011             $summary = $summary->params( $args )->inContentLanguage()->text();
03012         } else {
03013             $summary = wfMsgReplaceArgs( $summary, $args );
03014         }
03015 
03016         // Trim spaces on user supplied text
03017         $summary = trim( $summary );
03018 
03019         // Truncate for whole multibyte characters.
03020         $summary = $wgContLang->truncate( $summary, 255 );
03021 
03022         // Save
03023         $flags = EDIT_UPDATE;
03024 
03025         if ( $guser->isAllowed( 'minoredit' ) ) {
03026             $flags |= EDIT_MINOR;
03027         }
03028 
03029         if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
03030             $flags |= EDIT_FORCE_BOT;
03031         }
03032 
03033         // Actually store the edit
03034         $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
03035 
03036         if ( !$status->isOK() ) {
03037             return $status->getErrorsArray();
03038         }
03039 
03040         // raise error, when the edit is an edit without a new version
03041         if ( empty( $status->value['revision'] ) ) {
03042             $resultDetails = array( 'current' => $current );
03043             return array( array( 'alreadyrolled',
03044                     htmlspecialchars( $this->mTitle->getPrefixedText() ),
03045                     htmlspecialchars( $fromP ),
03046                     htmlspecialchars( $current->getUserText() )
03047             ) );
03048         }
03049 
03050         $revId = $status->value['revision']->getId();
03051 
03052         wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) );
03053 
03054         $resultDetails = array(
03055             'summary' => $summary,
03056             'current' => $current,
03057             'target' => $target,
03058             'newid' => $revId
03059         );
03060 
03061         return array();
03062     }
03063 
03075     public static function onArticleCreate( $title ) {
03076         // Update existence markers on article/talk tabs...
03077         if ( $title->isTalkPage() ) {
03078             $other = $title->getSubjectPage();
03079         } else {
03080             $other = $title->getTalkPage();
03081         }
03082 
03083         $other->invalidateCache();
03084         $other->purgeSquid();
03085 
03086         $title->touchLinks();
03087         $title->purgeSquid();
03088         $title->deleteTitleProtection();
03089     }
03090 
03096     public static function onArticleDelete( $title ) {
03097         // Update existence markers on article/talk tabs...
03098         if ( $title->isTalkPage() ) {
03099             $other = $title->getSubjectPage();
03100         } else {
03101             $other = $title->getTalkPage();
03102         }
03103 
03104         $other->invalidateCache();
03105         $other->purgeSquid();
03106 
03107         $title->touchLinks();
03108         $title->purgeSquid();
03109 
03110         // File cache
03111         HTMLFileCache::clearFileCache( $title );
03112         InfoAction::invalidateCache( $title );
03113 
03114         // Messages
03115         if ( $title->getNamespace() == NS_MEDIAWIKI ) {
03116             MessageCache::singleton()->replace( $title->getDBkey(), false );
03117         }
03118 
03119         // Images
03120         if ( $title->getNamespace() == NS_FILE ) {
03121             $update = new HTMLCacheUpdate( $title, 'imagelinks' );
03122             $update->doUpdate();
03123         }
03124 
03125         // User talk pages
03126         if ( $title->getNamespace() == NS_USER_TALK ) {
03127             $user = User::newFromName( $title->getText(), false );
03128             if ( $user ) {
03129                 $user->setNewtalk( false );
03130             }
03131         }
03132 
03133         // Image redirects
03134         RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
03135     }
03136 
03143     public static function onArticleEdit( $title ) {
03144         // Invalidate caches of articles which include this page
03145         DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' );
03146 
03147         // Invalidate the caches of all pages which redirect here
03148         DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' );
03149 
03150         // Purge squid for this page only
03151         $title->purgeSquid();
03152 
03153         // Clear file cache for this page only
03154         HTMLFileCache::clearFileCache( $title );
03155         InfoAction::invalidateCache( $title );
03156     }
03157 
03166     public function getCategories() {
03167         $id = $this->getId();
03168         if ( $id == 0 ) {
03169             return TitleArray::newFromResult( new FakeResultWrapper( array() ) );
03170         }
03171 
03172         $dbr = wfGetDB( DB_SLAVE );
03173         $res = $dbr->select( 'categorylinks',
03174             array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ),
03175             // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
03176             // as not being aliases, and NS_CATEGORY is numeric
03177             array( 'cl_from' => $id ),
03178             __METHOD__ );
03179 
03180         return TitleArray::newFromResult( $res );
03181     }
03182 
03189     public function getHiddenCategories() {
03190         $result = array();
03191         $id = $this->getId();
03192 
03193         if ( $id == 0 ) {
03194             return array();
03195         }
03196 
03197         $dbr = wfGetDB( DB_SLAVE );
03198         $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
03199             array( 'cl_to' ),
03200             array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
03201                 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ),
03202             __METHOD__ );
03203 
03204         if ( $res !== false ) {
03205             foreach ( $res as $row ) {
03206                 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
03207             }
03208         }
03209 
03210         return $result;
03211     }
03212 
03222     public static function getAutosummary( $oldtext, $newtext, $flags ) {
03223         // NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
03224 
03225         ContentHandler::deprecated( __METHOD__, '1.21' );
03226 
03227         $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
03228         $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
03229         $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
03230 
03231         return $handler->getAutosummary( $oldContent, $newContent, $flags );
03232     }
03233 
03241     public function getAutoDeleteReason( &$hasHistory ) {
03242         return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
03243     }
03244 
03252     public function updateCategoryCounts( array $added, array $deleted ) {
03253         $that = $this;
03254         $method = __METHOD__;
03255         $dbw = wfGetDB( DB_MASTER );
03256 
03257         // Do this at the end of the commit to reduce lock wait timeouts
03258         $dbw->onTransactionPreCommitOrIdle(
03259             function() use ( $dbw, $that, $method, $added, $deleted ) {
03260                 $ns = $that->getTitle()->getNamespace();
03261 
03262                 $addFields = array( 'cat_pages = cat_pages + 1' );
03263                 $removeFields = array( 'cat_pages = cat_pages - 1' );
03264                 if ( $ns == NS_CATEGORY ) {
03265                     $addFields[] = 'cat_subcats = cat_subcats + 1';
03266                     $removeFields[] = 'cat_subcats = cat_subcats - 1';
03267                 } elseif ( $ns == NS_FILE ) {
03268                     $addFields[] = 'cat_files = cat_files + 1';
03269                     $removeFields[] = 'cat_files = cat_files - 1';
03270                 }
03271 
03272                 if ( count( $added ) ) {
03273                     $insertRows = array();
03274                     foreach ( $added as $cat ) {
03275                         $insertRows[] = array(
03276                             'cat_title'   => $cat,
03277                             'cat_pages'   => 1,
03278                             'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
03279                             'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
03280                         );
03281                     }
03282                     $dbw->upsert(
03283                         'category',
03284                         $insertRows,
03285                         array( 'cat_title' ),
03286                         $addFields,
03287                         $method
03288                     );
03289                 }
03290 
03291                 if ( count( $deleted ) ) {
03292                     $dbw->update(
03293                         'category',
03294                         $removeFields,
03295                         array( 'cat_title' => $deleted ),
03296                         $method
03297                     );
03298                 }
03299 
03300                 foreach ( $added as $catName ) {
03301                     $cat = Category::newFromName( $catName );
03302                     wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $that ) );
03303                 }
03304 
03305                 foreach ( $deleted as $catName ) {
03306                     $cat = Category::newFromName( $catName );
03307                     wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $that ) );
03308                 }
03309             }
03310         );
03311     }
03312 
03318     public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) {
03319         if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) {
03320             return;
03321         }
03322 
03323         // templatelinks or imagelinks tables may have become out of sync,
03324         // especially if using variable-based transclusions.
03325         // For paranoia, check if things have changed and if
03326         // so apply updates to the database. This will ensure
03327         // that cascaded protections apply as soon as the changes
03328         // are visible.
03329 
03330         // Get templates from templatelinks and images from imagelinks
03331         $id = $this->getId();
03332 
03333         $dbLinks = array();
03334 
03335         $dbr = wfGetDB( DB_SLAVE );
03336         $res = $dbr->select( array( 'templatelinks' ),
03337             array( 'tl_namespace', 'tl_title' ),
03338             array( 'tl_from' => $id ),
03339             __METHOD__
03340         );
03341 
03342         foreach ( $res as $row ) {
03343             $dbLinks["{$row->tl_namespace}:{$row->tl_title}"] = true;
03344         }
03345 
03346         $dbr = wfGetDB( DB_SLAVE );
03347         $res = $dbr->select( array( 'imagelinks' ),
03348             array( 'il_to' ),
03349             array( 'il_from' => $id ),
03350             __METHOD__
03351         );
03352 
03353         foreach ( $res as $row ) {
03354             $dbLinks[NS_FILE . ":{$row->il_to}"] = true;
03355         }
03356 
03357         // Get templates and images from parser output.
03358         $poLinks = array();
03359         foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
03360             foreach ( $templates as $dbk => $id ) {
03361                 $poLinks["$ns:$dbk"] = true;
03362             }
03363         }
03364         foreach ( $parserOutput->getImages() as $dbk => $id ) {
03365             $poLinks[NS_FILE . ":$dbk"] = true;
03366         }
03367 
03368         // Get the diff
03369         $links_diff = array_diff_key( $poLinks, $dbLinks );
03370 
03371         if ( count( $links_diff ) > 0 ) {
03372             // Whee, link updates time.
03373             // Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output.
03374             $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
03375             $u->doUpdate();
03376         }
03377     }
03378 
03386     public function getUsedTemplates() {
03387         return $this->mTitle->getTemplateLinksFrom();
03388     }
03389 
03402     public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) {
03403         global $wgParser, $wgUser;
03404 
03405         wfDeprecated( __METHOD__, '1.19' );
03406 
03407         $user = is_null( $user ) ? $wgUser : $user;
03408 
03409         if ( $popts === null ) {
03410             $popts = ParserOptions::newFromUser( $user );
03411         }
03412 
03413         return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
03414     }
03415 
03422     public function isBigDeletion() {
03423         wfDeprecated( __METHOD__, '1.19' );
03424         return $this->mTitle->isBigDeletion();
03425     }
03426 
03433     public function estimateRevisionCount() {
03434         wfDeprecated( __METHOD__, '1.19' );
03435         return $this->mTitle->estimateRevisionCount();
03436     }
03437 
03449     public function updateRestrictions(
03450         $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null
03451     ) {
03452         global $wgUser;
03453 
03454         $user = is_null( $user ) ? $wgUser : $user;
03455 
03456         return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK();
03457     }
03458 
03466     public function getDeletionUpdates( Content $content = null ) {
03467         if ( !$content ) {
03468             // load content object, which may be used to determine the necessary updates
03469             // XXX: the content may not be needed to determine the updates, then this would be overhead.
03470             $content = $this->getContent( Revision::RAW );
03471         }
03472 
03473         if ( !$content ) {
03474             $updates = array();
03475         } else {
03476             $updates = $content->getDeletionUpdates( $this );
03477         }
03478 
03479         wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
03480         return $updates;
03481     }
03482 
03483 }
03484 
03485 class PoolWorkArticleView extends PoolCounterWork {
03486 
03490     private $page;
03491 
03495     private $cacheKey;
03496 
03500     private $revid;
03501 
03505     private $parserOptions;
03506 
03510     private $content = null;
03511 
03515     private $parserOutput = false;
03516 
03520     private $isDirty = false;
03521 
03525     private $error = false;
03526 
03536     public function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
03537         if ( is_string( $content ) ) { // BC: old style call
03538             $modelId = $page->getRevision()->getContentModel();
03539             $format = $page->getRevision()->getContentFormat();
03540             $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
03541         }
03542 
03543         $this->page = $page;
03544         $this->revid = $revid;
03545         $this->cacheable = $useParserCache;
03546         $this->parserOptions = $parserOptions;
03547         $this->content = $content;
03548         $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
03549         parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
03550     }
03551 
03557     public function getParserOutput() {
03558         return $this->parserOutput;
03559     }
03560 
03566     public function getIsDirty() {
03567         return $this->isDirty;
03568     }
03569 
03575     public function getError() {
03576         return $this->error;
03577     }
03578 
03582     public function doWork() {
03583         global $wgUseFileCache;
03584 
03585         // @todo several of the methods called on $this->page are not declared in Page, but present
03586         //        in WikiPage and delegated by Article.
03587 
03588         $isCurrent = $this->revid === $this->page->getLatest();
03589 
03590         if ( $this->content !== null ) {
03591             $content = $this->content;
03592         } elseif ( $isCurrent ) {
03593             // XXX: why use RAW audience here, and PUBLIC (default) below?
03594             $content = $this->page->getContent( Revision::RAW );
03595         } else {
03596             $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
03597 
03598             if ( $rev === null ) {
03599                 $content = null;
03600             } else {
03601                 // XXX: why use PUBLIC audience here (default), and RAW above?
03602                 $content = $rev->getContent();
03603             }
03604         }
03605 
03606         if ( $content === null ) {
03607             return false;
03608         }
03609 
03610         // Reduce effects of race conditions for slow parses (bug 46014)
03611         $cacheTime = wfTimestampNow();
03612 
03613         $time = - microtime( true );
03614         $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
03615         $time += microtime( true );
03616 
03617         // Timing hack
03618         if ( $time > 3 ) {
03619             wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time,
03620                 $this->page->getTitle()->getPrefixedDBkey() ) );
03621         }
03622 
03623         if ( $this->cacheable && $this->parserOutput->isCacheable() && $isCurrent ) {
03624             ParserCache::singleton()->save(
03625                 $this->parserOutput, $this->page, $this->parserOptions, $cacheTime, $this->revid );
03626         }
03627 
03628         // Make sure file cache is not used on uncacheable content.
03629         // Output that has magic words in it can still use the parser cache
03630         // (if enabled), though it will generally expire sooner.
03631         if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) {
03632             $wgUseFileCache = false;
03633         }
03634 
03635         if ( $isCurrent ) {
03636             $this->page->doCascadeProtectionUpdates( $this->parserOutput );
03637         }
03638 
03639         return true;
03640     }
03641 
03645     public function getCachedWork() {
03646         $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions );
03647 
03648         if ( $this->parserOutput === false ) {
03649             wfDebug( __METHOD__ . ": parser cache miss\n" );
03650             return false;
03651         } else {
03652             wfDebug( __METHOD__ . ": parser cache hit\n" );
03653             return true;
03654         }
03655     }
03656 
03660     public function fallback() {
03661         $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions );
03662 
03663         if ( $this->parserOutput === false ) {
03664             wfDebugLog( 'dirty', 'dirty missing' );
03665             wfDebug( __METHOD__ . ": no dirty cache\n" );
03666             return false;
03667         } else {
03668             wfDebug( __METHOD__ . ": sending dirty output\n" );
03669             wfDebugLog( 'dirty', "dirty output {$this->cacheKey}" );
03670             $this->isDirty = true;
03671             return true;
03672         }
03673     }
03674 
03679     public function error( $status ) {
03680         $this->error = $status;
03681         return false;
03682     }
03683 }