MediaWiki
REL1_23
|
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 }