MediaWiki
master
|
00001 <?php 00026 interface Page {} 00027 00036 class WikiPage implements Page, IDBAccessObject { 00037 // Constants for $mDataLoadedFrom and related 00038 00042 public $mTitle = null; 00043 00047 public $mDataLoaded = false; // !< Boolean 00048 public $mIsRedirect = false; // !< Boolean 00049 public $mLatest = false; // !< Integer (false means "not loaded") 00050 public $mPreparedEdit = false; // !< Array 00056 protected $mDataLoadedFrom = self::READ_NONE; 00057 00061 protected $mRedirectTarget = null; 00062 00066 protected $mLastRevision = null; 00067 00071 protected $mTimestamp = ''; 00072 00076 protected $mTouched = '19700101000000'; 00077 00081 protected $mCounter = null; 00082 00087 public function __construct( Title $title ) { 00088 $this->mTitle = $title; 00089 } 00090 00098 public static function factory( Title $title ) { 00099 $ns = $title->getNamespace(); 00100 00101 if ( $ns == NS_MEDIA ) { 00102 throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." ); 00103 } elseif ( $ns < 0 ) { 00104 throw new MWException( "Invalid or virtual namespace $ns given." ); 00105 } 00106 00107 switch ( $ns ) { 00108 case NS_FILE: 00109 $page = new WikiFilePage( $title ); 00110 break; 00111 case NS_CATEGORY: 00112 $page = new WikiCategoryPage( $title ); 00113 break; 00114 default: 00115 $page = new WikiPage( $title ); 00116 } 00117 00118 return $page; 00119 } 00120 00131 public static function newFromID( $id, $from = 'fromdb' ) { 00132 $from = self::convertSelectType( $from ); 00133 $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE ); 00134 $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ ); 00135 if ( !$row ) { 00136 return null; 00137 } 00138 return self::newFromRow( $row, $from ); 00139 } 00140 00153 public static function newFromRow( $row, $from = 'fromdb' ) { 00154 $page = self::factory( Title::newFromRow( $row ) ); 00155 $page->loadFromRow( $row, $from ); 00156 return $page; 00157 } 00158 00165 private static function convertSelectType( $type ) { 00166 switch ( $type ) { 00167 case 'fromdb': 00168 return self::READ_NORMAL; 00169 case 'fromdbmaster': 00170 return self::READ_LATEST; 00171 case 'forupdate': 00172 return self::READ_LOCKING; 00173 default: 00174 // It may already be an integer or whatever else 00175 return $type; 00176 } 00177 } 00178 00189 public function getActionOverrides() { 00190 $content_handler = $this->getContentHandler(); 00191 return $content_handler->getActionOverrides(); 00192 } 00193 00203 public function getContentHandler() { 00204 return ContentHandler::getForModelID( $this->getContentModel() ); 00205 } 00206 00211 public function getTitle() { 00212 return $this->mTitle; 00213 } 00214 00219 public function clear() { 00220 $this->mDataLoaded = false; 00221 $this->mDataLoadedFrom = self::READ_NONE; 00222 00223 $this->clearCacheFields(); 00224 } 00225 00230 protected function clearCacheFields() { 00231 $this->mCounter = null; 00232 $this->mRedirectTarget = null; // Title object if set 00233 $this->mLastRevision = null; // Latest revision 00234 $this->mTouched = '19700101000000'; 00235 $this->mTimestamp = ''; 00236 $this->mIsRedirect = false; 00237 $this->mLatest = false; 00238 $this->mPreparedEdit = false; 00239 } 00240 00247 public static function selectFields() { 00248 global $wgContentHandlerUseDB; 00249 00250 $fields = array( 00251 'page_id', 00252 'page_namespace', 00253 'page_title', 00254 'page_restrictions', 00255 'page_counter', 00256 'page_is_redirect', 00257 'page_is_new', 00258 'page_random', 00259 'page_touched', 00260 'page_latest', 00261 'page_len', 00262 ); 00263 00264 if ( $wgContentHandlerUseDB ) { 00265 $fields[] = 'page_content_model'; 00266 } 00267 00268 return $fields; 00269 } 00270 00278 protected function pageData( $dbr, $conditions, $options = array() ) { 00279 $fields = self::selectFields(); 00280 00281 wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); 00282 00283 $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); 00284 00285 wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); 00286 00287 return $row; 00288 } 00289 00299 public function pageDataFromTitle( $dbr, $title, $options = array() ) { 00300 return $this->pageData( $dbr, array( 00301 'page_namespace' => $title->getNamespace(), 00302 'page_title' => $title->getDBkey() ), $options ); 00303 } 00304 00313 public function pageDataFromId( $dbr, $id, $options = array() ) { 00314 return $this->pageData( $dbr, array( 'page_id' => $id ), $options ); 00315 } 00316 00329 public function loadPageData( $from = 'fromdb' ) { 00330 $from = self::convertSelectType( $from ); 00331 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) { 00332 // We already have the data from the correct location, no need to load it twice. 00333 return; 00334 } 00335 00336 if ( $from === self::READ_LOCKING ) { 00337 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); 00338 } elseif ( $from === self::READ_LATEST ) { 00339 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00340 } elseif ( $from === self::READ_NORMAL ) { 00341 $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); 00342 // Use a "last rev inserted" timestamp key to dimish the issue of slave lag. 00343 // Note that DB also stores the master position in the session and checks it. 00344 $touched = $this->getCachedLastEditTime(); 00345 if ( $touched ) { // key set 00346 if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { 00347 $from = self::READ_LATEST; 00348 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00349 } 00350 } 00351 } else { 00352 // No idea from where the caller got this data, assume slave database. 00353 $data = $from; 00354 $from = self::READ_NORMAL; 00355 } 00356 00357 $this->loadFromRow( $data, $from ); 00358 } 00359 00372 public function loadFromRow( $data, $from ) { 00373 $lc = LinkCache::singleton(); 00374 00375 if ( $data ) { 00376 $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); 00377 00378 $this->mTitle->loadFromRow( $data ); 00379 00380 // Old-fashioned restrictions 00381 $this->mTitle->loadRestrictions( $data->page_restrictions ); 00382 00383 $this->mCounter = intval( $data->page_counter ); 00384 $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); 00385 $this->mIsRedirect = intval( $data->page_is_redirect ); 00386 $this->mLatest = intval( $data->page_latest ); 00387 // Bug 37225: $latest may no longer match the cached latest Revision object. 00388 // Double-check the ID of any cached latest Revision object for consistency. 00389 if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { 00390 $this->mLastRevision = null; 00391 $this->mTimestamp = ''; 00392 } 00393 } else { 00394 $lc->addBadLinkObj( $this->mTitle ); 00395 00396 $this->mTitle->loadFromRow( false ); 00397 00398 $this->clearCacheFields(); 00399 } 00400 00401 $this->mDataLoaded = true; 00402 $this->mDataLoadedFrom = self::convertSelectType( $from ); 00403 } 00404 00408 public function getId() { 00409 return $this->mTitle->getArticleID(); 00410 } 00411 00415 public function exists() { 00416 return $this->mTitle->exists(); 00417 } 00418 00427 public function hasViewableContent() { 00428 return $this->mTitle->exists() || $this->mTitle->isAlwaysKnown(); 00429 } 00430 00434 public function getCount() { 00435 if ( !$this->mDataLoaded ) { 00436 $this->loadPageData(); 00437 } 00438 00439 return $this->mCounter; 00440 } 00441 00447 public function isRedirect( ) { 00448 $content = $this->getContent(); 00449 if ( !$content ) return false; 00450 00451 return $content->isRedirect(); 00452 } 00453 00464 public function getContentModel() { 00465 if ( $this->exists() ) { 00466 // look at the revision's actual content model 00467 $rev = $this->getRevision(); 00468 00469 if ( $rev !== null ) { 00470 return $rev->getContentModel(); 00471 } else { 00472 $title = $this->mTitle->getPrefixedDBkey(); 00473 wfWarn( "Page $title exists but has no (visible) revisions!" ); 00474 } 00475 } 00476 00477 // use the default model for this page 00478 return $this->mTitle->getContentModel(); 00479 } 00480 00485 public function checkTouched() { 00486 if ( !$this->mDataLoaded ) { 00487 $this->loadPageData(); 00488 } 00489 return !$this->mIsRedirect; 00490 } 00491 00496 public function getTouched() { 00497 if ( !$this->mDataLoaded ) { 00498 $this->loadPageData(); 00499 } 00500 return $this->mTouched; 00501 } 00502 00507 public function getLatest() { 00508 if ( !$this->mDataLoaded ) { 00509 $this->loadPageData(); 00510 } 00511 return (int)$this->mLatest; 00512 } 00513 00518 public function getOldestRevision() { 00519 wfProfileIn( __METHOD__ ); 00520 00521 // Try using the slave database first, then try the master 00522 $continue = 2; 00523 $db = wfGetDB( DB_SLAVE ); 00524 $revSelectFields = Revision::selectFields(); 00525 00526 while ( $continue ) { 00527 $row = $db->selectRow( 00528 array( 'page', 'revision' ), 00529 $revSelectFields, 00530 array( 00531 'page_namespace' => $this->mTitle->getNamespace(), 00532 'page_title' => $this->mTitle->getDBkey(), 00533 'rev_page = page_id' 00534 ), 00535 __METHOD__, 00536 array( 00537 'ORDER BY' => 'rev_timestamp ASC' 00538 ) 00539 ); 00540 00541 if ( $row ) { 00542 $continue = 0; 00543 } else { 00544 $db = wfGetDB( DB_MASTER ); 00545 $continue--; 00546 } 00547 } 00548 00549 wfProfileOut( __METHOD__ ); 00550 return $row ? Revision::newFromRow( $row ) : null; 00551 } 00552 00557 protected function loadLastEdit() { 00558 if ( $this->mLastRevision !== null ) { 00559 return; // already loaded 00560 } 00561 00562 $latest = $this->getLatest(); 00563 if ( !$latest ) { 00564 return; // page doesn't exist or is missing page_latest info 00565 } 00566 00567 // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the 00568 // latest changes committed. This is true even within REPEATABLE-READ transactions, where 00569 // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to 00570 // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row 00571 // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. 00572 // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. 00573 $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; 00574 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); 00575 if ( $revision ) { // sanity 00576 $this->setLastEdit( $revision ); 00577 } 00578 } 00579 00583 protected function setLastEdit( Revision $revision ) { 00584 $this->mLastRevision = $revision; 00585 $this->mTimestamp = $revision->getTimestamp(); 00586 } 00587 00592 public function getRevision() { 00593 $this->loadLastEdit(); 00594 if ( $this->mLastRevision ) { 00595 return $this->mLastRevision; 00596 } 00597 return null; 00598 } 00599 00613 public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00614 $this->loadLastEdit(); 00615 if ( $this->mLastRevision ) { 00616 return $this->mLastRevision->getContent( $audience, $user ); 00617 } 00618 return null; 00619 } 00620 00633 public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { // @todo: deprecated, replace usage! 00634 ContentHandler::deprecated( __METHOD__, '1.21' ); 00635 00636 $this->loadLastEdit(); 00637 if ( $this->mLastRevision ) { 00638 return $this->mLastRevision->getText( $audience, $user ); 00639 } 00640 return false; 00641 } 00642 00649 public function getRawText() { 00650 ContentHandler::deprecated( __METHOD__, '1.21' ); 00651 00652 return $this->getText( Revision::RAW ); 00653 } 00654 00658 public function getTimestamp() { 00659 // Check if the field has been filled by WikiPage::setTimestamp() 00660 if ( !$this->mTimestamp ) { 00661 $this->loadLastEdit(); 00662 } 00663 00664 return wfTimestamp( TS_MW, $this->mTimestamp ); 00665 } 00666 00672 public function setTimestamp( $ts ) { 00673 $this->mTimestamp = wfTimestamp( TS_MW, $ts ); 00674 } 00675 00685 public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00686 $this->loadLastEdit(); 00687 if ( $this->mLastRevision ) { 00688 return $this->mLastRevision->getUser( $audience, $user ); 00689 } else { 00690 return -1; 00691 } 00692 } 00693 00704 public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00705 $revision = $this->getOldestRevision(); 00706 if ( $revision ) { 00707 $userName = $revision->getUserText( $audience, $user ); 00708 return User::newFromName( $userName, false ); 00709 } else { 00710 return null; 00711 } 00712 } 00713 00723 public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00724 $this->loadLastEdit(); 00725 if ( $this->mLastRevision ) { 00726 return $this->mLastRevision->getUserText( $audience, $user ); 00727 } else { 00728 return ''; 00729 } 00730 } 00731 00741 public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00742 $this->loadLastEdit(); 00743 if ( $this->mLastRevision ) { 00744 return $this->mLastRevision->getComment( $audience, $user ); 00745 } else { 00746 return ''; 00747 } 00748 } 00749 00755 public function getMinorEdit() { 00756 $this->loadLastEdit(); 00757 if ( $this->mLastRevision ) { 00758 return $this->mLastRevision->isMinor(); 00759 } else { 00760 return false; 00761 } 00762 } 00763 00769 protected function getCachedLastEditTime() { 00770 global $wgMemc; 00771 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00772 return $wgMemc->get( $key ); 00773 } 00774 00781 public function setCachedLastEditTime( $timestamp ) { 00782 global $wgMemc; 00783 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00784 $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60*15 ); 00785 } 00786 00795 public function isCountable( $editInfo = false ) { 00796 global $wgArticleCountMethod; 00797 00798 if ( !$this->mTitle->isContentPage() ) { 00799 return false; 00800 } 00801 00802 if ( $editInfo ) { 00803 $content = $editInfo->pstContent; 00804 } else { 00805 $content = $this->getContent(); 00806 } 00807 00808 if ( !$content || $content->isRedirect( ) ) { 00809 return false; 00810 } 00811 00812 $hasLinks = null; 00813 00814 if ( $wgArticleCountMethod === 'link' ) { 00815 // nasty special case to avoid re-parsing to detect links 00816 00817 if ( $editInfo ) { 00818 // ParserOutput::getLinks() is a 2D array of page links, so 00819 // to be really correct we would need to recurse in the array 00820 // but the main array should only have items in it if there are 00821 // links. 00822 $hasLinks = (bool)count( $editInfo->output->getLinks() ); 00823 } else { 00824 $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, 00825 array( 'pl_from' => $this->getId() ), __METHOD__ ); 00826 } 00827 } 00828 00829 return $content->isCountable( $hasLinks ); 00830 } 00831 00839 public function getRedirectTarget() { 00840 if ( !$this->mTitle->isRedirect() ) { 00841 return null; 00842 } 00843 00844 if ( $this->mRedirectTarget !== null ) { 00845 return $this->mRedirectTarget; 00846 } 00847 00848 // Query the redirect table 00849 $dbr = wfGetDB( DB_SLAVE ); 00850 $row = $dbr->selectRow( 'redirect', 00851 array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), 00852 array( 'rd_from' => $this->getId() ), 00853 __METHOD__ 00854 ); 00855 00856 // rd_fragment and rd_interwiki were added later, populate them if empty 00857 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { 00858 return $this->mRedirectTarget = Title::makeTitle( 00859 $row->rd_namespace, $row->rd_title, 00860 $row->rd_fragment, $row->rd_interwiki ); 00861 } 00862 00863 // This page doesn't have an entry in the redirect table 00864 return $this->mRedirectTarget = $this->insertRedirect(); 00865 } 00866 00873 public function insertRedirect() { 00874 // recurse through to only get the final target 00875 $content = $this->getContent(); 00876 $retval = $content ? $content->getUltimateRedirectTarget() : null; 00877 if ( !$retval ) { 00878 return null; 00879 } 00880 $this->insertRedirectEntry( $retval ); 00881 return $retval; 00882 } 00883 00889 public function insertRedirectEntry( $rt ) { 00890 $dbw = wfGetDB( DB_MASTER ); 00891 $dbw->replace( 'redirect', array( 'rd_from' ), 00892 array( 00893 'rd_from' => $this->getId(), 00894 'rd_namespace' => $rt->getNamespace(), 00895 'rd_title' => $rt->getDBkey(), 00896 'rd_fragment' => $rt->getFragment(), 00897 'rd_interwiki' => $rt->getInterwiki(), 00898 ), 00899 __METHOD__ 00900 ); 00901 } 00902 00908 public function followRedirect() { 00909 return $this->getRedirectURL( $this->getRedirectTarget() ); 00910 } 00911 00919 public function getRedirectURL( $rt ) { 00920 if ( !$rt ) { 00921 return false; 00922 } 00923 00924 if ( $rt->isExternal() ) { 00925 if ( $rt->isLocal() ) { 00926 // Offsite wikis need an HTTP redirect. 00927 // 00928 // This can be hard to reverse and may produce loops, 00929 // so they may be disabled in the site configuration. 00930 $source = $this->mTitle->getFullURL( 'redirect=no' ); 00931 return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); 00932 } else { 00933 // External pages pages without "local" bit set are not valid 00934 // redirect targets 00935 return false; 00936 } 00937 } 00938 00939 if ( $rt->isSpecialPage() ) { 00940 // Gotta handle redirects to special pages differently: 00941 // Fill the HTTP response "Location" header and ignore 00942 // the rest of the page we're on. 00943 // 00944 // Some pages are not valid targets 00945 if ( $rt->isValidRedirectTarget() ) { 00946 return $rt->getFullURL(); 00947 } else { 00948 return false; 00949 } 00950 } 00951 00952 return $rt; 00953 } 00954 00960 public function getContributors() { 00961 // @todo FIXME: This is expensive; cache this info somewhere. 00962 00963 $dbr = wfGetDB( DB_SLAVE ); 00964 00965 if ( $dbr->implicitGroupby() ) { 00966 $realNameField = 'user_real_name'; 00967 } else { 00968 $realNameField = 'MIN(user_real_name) AS user_real_name'; 00969 } 00970 00971 $tables = array( 'revision', 'user' ); 00972 00973 $fields = array( 00974 'user_id' => 'rev_user', 00975 'user_name' => 'rev_user_text', 00976 $realNameField, 00977 'timestamp' => 'MAX(rev_timestamp)', 00978 ); 00979 00980 $conds = array( 'rev_page' => $this->getId() ); 00981 00982 // The user who made the top revision gets credited as "this page was last edited by 00983 // John, based on contributions by Tom, Dick and Harry", so don't include them twice. 00984 $user = $this->getUser(); 00985 if ( $user ) { 00986 $conds[] = "rev_user != $user"; 00987 } else { 00988 $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}"; 00989 } 00990 00991 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden? 00992 00993 $jconds = array( 00994 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ), 00995 ); 00996 00997 $options = array( 00998 'GROUP BY' => array( 'rev_user', 'rev_user_text' ), 00999 'ORDER BY' => 'timestamp DESC', 01000 ); 01001 01002 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds ); 01003 return new UserArrayFromResult( $res ); 01004 } 01005 01012 public function getLastNAuthors( $num, $revLatest = 0 ) { 01013 wfProfileIn( __METHOD__ ); 01014 // First try the slave 01015 // If that doesn't have the latest revision, try the master 01016 $continue = 2; 01017 $db = wfGetDB( DB_SLAVE ); 01018 01019 do { 01020 $res = $db->select( array( 'page', 'revision' ), 01021 array( 'rev_id', 'rev_user_text' ), 01022 array( 01023 'page_namespace' => $this->mTitle->getNamespace(), 01024 'page_title' => $this->mTitle->getDBkey(), 01025 'rev_page = page_id' 01026 ), __METHOD__, 01027 array( 01028 'ORDER BY' => 'rev_timestamp DESC', 01029 'LIMIT' => $num 01030 ) 01031 ); 01032 01033 if ( !$res ) { 01034 wfProfileOut( __METHOD__ ); 01035 return array(); 01036 } 01037 01038 $row = $db->fetchObject( $res ); 01039 01040 if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { 01041 $db = wfGetDB( DB_MASTER ); 01042 $continue--; 01043 } else { 01044 $continue = 0; 01045 } 01046 } while ( $continue ); 01047 01048 $authors = array( $row->rev_user_text ); 01049 01050 foreach ( $res as $row ) { 01051 $authors[] = $row->rev_user_text; 01052 } 01053 01054 wfProfileOut( __METHOD__ ); 01055 return $authors; 01056 } 01057 01065 public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { 01066 global $wgEnableParserCache; 01067 01068 return $wgEnableParserCache 01069 && $parserOptions->getStubThreshold() == 0 01070 && $this->mTitle->exists() 01071 && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) 01072 && $this->getContentHandler()->isParserCacheSupported(); 01073 } 01074 01086 public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { 01087 wfProfileIn( __METHOD__ ); 01088 01089 $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); 01090 wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); 01091 if ( $parserOptions->getStubThreshold() ) { 01092 wfIncrStats( 'pcache_miss_stub' ); 01093 } 01094 01095 if ( $useParserCache ) { 01096 $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); 01097 if ( $parserOutput !== false ) { 01098 wfProfileOut( __METHOD__ ); 01099 return $parserOutput; 01100 } 01101 } 01102 01103 if ( $oldid === null || $oldid === 0 ) { 01104 $oldid = $this->getLatest(); 01105 } 01106 01107 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache ); 01108 $pool->execute(); 01109 01110 wfProfileOut( __METHOD__ ); 01111 01112 return $pool->getParserOutput(); 01113 } 01114 01119 public function doViewUpdates( User $user ) { 01120 global $wgDisableCounters; 01121 if ( wfReadOnly() ) { 01122 return; 01123 } 01124 01125 // Don't update page view counters on views from bot users (bug 14044) 01126 if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->mTitle->exists() ) { 01127 DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); 01128 DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); 01129 } 01130 01131 // Update newtalk / watchlist notification status 01132 $user->clearNotification( $this->mTitle ); 01133 } 01134 01139 public function doPurge() { 01140 global $wgUseSquid; 01141 01142 if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { 01143 return false; 01144 } 01145 01146 // Invalidate the cache 01147 $this->mTitle->invalidateCache(); 01148 $this->clear(); 01149 01150 if ( $wgUseSquid ) { 01151 // Commit the transaction before the purge is sent 01152 $dbw = wfGetDB( DB_MASTER ); 01153 $dbw->commit( __METHOD__ ); 01154 01155 // Send purge 01156 $update = SquidUpdate::newSimplePurge( $this->mTitle ); 01157 $update->doUpdate(); 01158 } 01159 01160 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 01161 // @todo: move this logic to MessageCache 01162 01163 if ( $this->mTitle->exists() ) { 01164 // NOTE: use transclusion text for messages. 01165 // This is consistent with MessageCache::getMsgFromNamespace() 01166 01167 $content = $this->getContent(); 01168 $text = $content === null ? null : $content->getWikitextForTransclusion(); 01169 01170 if ( $text === null ) $text = false; 01171 } else { 01172 $text = false; 01173 } 01174 01175 MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); 01176 } 01177 return true; 01178 } 01179 01190 public function insertOn( $dbw ) { 01191 wfProfileIn( __METHOD__ ); 01192 01193 $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); 01194 $dbw->insert( 'page', array( 01195 'page_id' => $page_id, 01196 'page_namespace' => $this->mTitle->getNamespace(), 01197 'page_title' => $this->mTitle->getDBkey(), 01198 'page_counter' => 0, 01199 'page_restrictions' => '', 01200 'page_is_redirect' => 0, // Will set this shortly... 01201 'page_is_new' => 1, 01202 'page_random' => wfRandom(), 01203 'page_touched' => $dbw->timestamp(), 01204 'page_latest' => 0, // Fill this in shortly... 01205 'page_len' => 0, // Fill this in shortly... 01206 ), __METHOD__, 'IGNORE' ); 01207 01208 $affected = $dbw->affectedRows(); 01209 01210 if ( $affected ) { 01211 $newid = $dbw->insertId(); 01212 $this->mTitle->resetArticleID( $newid ); 01213 } 01214 wfProfileOut( __METHOD__ ); 01215 01216 return $affected ? $newid : false; 01217 } 01218 01234 public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { 01235 global $wgContentHandlerUseDB; 01236 01237 wfProfileIn( __METHOD__ ); 01238 01239 $content = $revision->getContent(); 01240 $len = $content ? $content->getSize() : 0; 01241 $rt = $content ? $content->getUltimateRedirectTarget() : null; 01242 01243 $conditions = array( 'page_id' => $this->getId() ); 01244 01245 if ( !is_null( $lastRevision ) ) { 01246 // An extra check against threads stepping on each other 01247 $conditions['page_latest'] = $lastRevision; 01248 } 01249 01250 $now = wfTimestampNow(); 01251 $row = array( /* SET */ 01252 'page_latest' => $revision->getId(), 01253 'page_touched' => $dbw->timestamp( $now ), 01254 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, 01255 'page_is_redirect' => $rt !== null ? 1 : 0, 01256 'page_len' => $len, 01257 ); 01258 01259 if ( $wgContentHandlerUseDB ) { 01260 $row[ 'page_content_model' ] = $revision->getContentModel(); 01261 } 01262 01263 $dbw->update( 'page', 01264 $row, 01265 $conditions, 01266 __METHOD__ ); 01267 01268 $result = $dbw->affectedRows() > 0; 01269 if ( $result ) { 01270 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); 01271 $this->setLastEdit( $revision ); 01272 $this->setCachedLastEditTime( $now ); 01273 $this->mLatest = $revision->getId(); 01274 $this->mIsRedirect = (bool)$rt; 01275 // Update the LinkCache. 01276 LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, 01277 $this->mLatest, $revision->getContentModel() ); 01278 } 01279 01280 wfProfileOut( __METHOD__ ); 01281 return $result; 01282 } 01283 01295 public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) { 01296 // Always update redirects (target link might have changed) 01297 // Update/Insert if we don't know if the last revision was a redirect or not 01298 // Delete if changing from redirect to non-redirect 01299 $isRedirect = !is_null( $redirectTitle ); 01300 01301 if ( !$isRedirect && $lastRevIsRedirect === false ) { 01302 return true; 01303 } 01304 01305 wfProfileIn( __METHOD__ ); 01306 if ( $isRedirect ) { 01307 $this->insertRedirectEntry( $redirectTitle ); 01308 } else { 01309 // This is not a redirect, remove row from redirect table 01310 $where = array( 'rd_from' => $this->getId() ); 01311 $dbw->delete( 'redirect', $where, __METHOD__ ); 01312 } 01313 01314 if ( $this->getTitle()->getNamespace() == NS_FILE ) { 01315 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); 01316 } 01317 wfProfileOut( __METHOD__ ); 01318 01319 return ( $dbw->affectedRows() != 0 ); 01320 } 01321 01330 public function updateIfNewerOn( $dbw, $revision ) { 01331 wfProfileIn( __METHOD__ ); 01332 01333 $row = $dbw->selectRow( 01334 array( 'revision', 'page' ), 01335 array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), 01336 array( 01337 'page_id' => $this->getId(), 01338 'page_latest=rev_id' ), 01339 __METHOD__ ); 01340 01341 if ( $row ) { 01342 if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { 01343 wfProfileOut( __METHOD__ ); 01344 return false; 01345 } 01346 $prev = $row->rev_id; 01347 $lastRevIsRedirect = (bool)$row->page_is_redirect; 01348 } else { 01349 // No or missing previous revision; mark the page as new 01350 $prev = 0; 01351 $lastRevIsRedirect = null; 01352 } 01353 01354 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); 01355 01356 wfProfileOut( __METHOD__ ); 01357 return $ret; 01358 } 01359 01370 public function getUndoContent( Revision $undo, Revision $undoafter = null ) { 01371 $handler = $undo->getContentHandler(); 01372 return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter ); 01373 } 01374 01384 public function getUndoText( Revision $undo, Revision $undoafter = null ) { 01385 ContentHandler::deprecated( __METHOD__, '1.21' ); 01386 01387 $this->loadLastEdit(); 01388 01389 if ( $this->mLastRevision ) { 01390 if ( is_null( $undoafter ) ) { 01391 $undoafter = $undo->getPrevious(); 01392 } 01393 01394 $handler = $this->getContentHandler(); 01395 $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); 01396 01397 if ( !$undone ) { 01398 return false; 01399 } else { 01400 return ContentHandler::getContentText( $undone ); 01401 } 01402 } 01403 01404 return false; 01405 } 01406 01417 public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { 01418 ContentHandler::deprecated( __METHOD__, '1.21' ); 01419 01420 if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent! 01421 // Whole-page edit; let the whole text through 01422 return $text; 01423 } 01424 01425 if ( !$this->supportsSections() ) { 01426 throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); 01427 } 01428 01429 // could even make section title, but that's not required. 01430 $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); 01431 01432 $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime ); 01433 01434 return ContentHandler::getContentText( $newContent ); 01435 } 01436 01445 public function supportsSections() { 01446 return $this->getContentHandler()->supportsSections(); 01447 } 01448 01460 public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) { 01461 wfProfileIn( __METHOD__ ); 01462 01463 if ( strval( $section ) == '' ) { 01464 // Whole-page edit; let the whole text through 01465 $newContent = $sectionContent; 01466 } else { 01467 if ( !$this->supportsSections() ) { 01468 throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); 01469 } 01470 01471 // Bug 30711: always use current version when adding a new section 01472 if ( is_null( $edittime ) || $section == 'new' ) { 01473 $oldContent = $this->getContent(); 01474 } else { 01475 $dbw = wfGetDB( DB_MASTER ); 01476 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); 01477 01478 if ( !$rev ) { 01479 wfDebug( "WikiPage::replaceSection asked for bogus section (page: " . 01480 $this->getId() . "; section: $section; edittime: $edittime)\n" ); 01481 wfProfileOut( __METHOD__ ); 01482 return null; 01483 } 01484 01485 $oldContent = $rev->getContent(); 01486 } 01487 01488 if ( ! $oldContent ) { 01489 wfDebug( __METHOD__ . ": no page text\n" ); 01490 wfProfileOut( __METHOD__ ); 01491 return null; 01492 } 01493 01494 // FIXME: $oldContent might be null? 01495 $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle ); 01496 } 01497 01498 wfProfileOut( __METHOD__ ); 01499 return $newContent; 01500 } 01501 01507 function checkFlags( $flags ) { 01508 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { 01509 if ( $this->mTitle->getArticleID() ) { 01510 $flags |= EDIT_UPDATE; 01511 } else { 01512 $flags |= EDIT_NEW; 01513 } 01514 } 01515 01516 return $flags; 01517 } 01518 01568 public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { 01569 ContentHandler::deprecated( __METHOD__, '1.21' ); 01570 01571 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 01572 01573 return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); 01574 } 01575 01624 public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, 01625 User $user = null, $serialisation_format = null ) { 01626 global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; 01627 01628 // Low-level sanity check 01629 if ( $this->mTitle->getText() === '' ) { 01630 throw new MWException( 'Something is trying to edit an article with an empty title' ); 01631 } 01632 01633 wfProfileIn( __METHOD__ ); 01634 01635 if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { 01636 wfProfileOut( __METHOD__ ); 01637 return Status::newFatal( 'content-not-allowed-here', 01638 ContentHandler::getLocalizedName( $content->getModel() ), 01639 $this->getTitle()->getPrefixedText() ); 01640 } 01641 01642 $user = is_null( $user ) ? $wgUser : $user; 01643 $status = Status::newGood( array() ); 01644 01645 // Load the data from the master database if needed. 01646 // The caller may already loaded it from the master or even loaded it using 01647 // SELECT FOR UPDATE, so do not override that using clear(). 01648 $this->loadPageData( 'fromdbmaster' ); 01649 01650 $flags = $this->checkFlags( $flags ); 01651 01652 // handle hook 01653 $hook_args = array( &$this, &$user, &$content, &$summary, 01654 $flags & EDIT_MINOR, null, null, &$flags, &$status ); 01655 01656 if ( !wfRunHooks( 'PageContentSave', $hook_args ) 01657 || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { 01658 01659 wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); 01660 01661 if ( $status->isOK() ) { 01662 $status->fatal( 'edit-hook-aborted' ); 01663 } 01664 01665 wfProfileOut( __METHOD__ ); 01666 return $status; 01667 } 01668 01669 // Silently ignore EDIT_MINOR if not allowed 01670 $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); 01671 $bot = $flags & EDIT_FORCE_BOT; 01672 01673 $old_content = $this->getContent( Revision::RAW ); // current revision's content 01674 01675 $oldsize = $old_content ? $old_content->getSize() : 0; 01676 $oldid = $this->getLatest(); 01677 $oldIsRedirect = $this->isRedirect(); 01678 $oldcountable = $this->isCountable(); 01679 01680 $handler = $content->getContentHandler(); 01681 01682 // Provide autosummaries if one is not provided and autosummaries are enabled. 01683 if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { 01684 if ( !$old_content ) $old_content = null; 01685 $summary = $handler->getAutosummary( $old_content, $content, $flags ); 01686 } 01687 01688 $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); 01689 $serialized = $editInfo->pst; 01690 $content = $editInfo->pstContent; 01691 $newsize = $content->getSize(); 01692 01693 $dbw = wfGetDB( DB_MASTER ); 01694 $now = wfTimestampNow(); 01695 $this->mTimestamp = $now; 01696 01697 if ( $flags & EDIT_UPDATE ) { 01698 // Update article, but only if changed. 01699 $status->value['new'] = false; 01700 01701 if ( !$oldid ) { 01702 // Article gone missing 01703 wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); 01704 $status->fatal( 'edit-gone-missing' ); 01705 01706 wfProfileOut( __METHOD__ ); 01707 return $status; 01708 } elseif ( !$old_content ) { 01709 // Sanity check for bug 37225 01710 wfProfileOut( __METHOD__ ); 01711 throw new MWException( "Could not find text for current revision {$oldid}." ); 01712 } 01713 01714 $revision = new Revision( array( 01715 'page' => $this->getId(), 01716 'title' => $this->getTitle(), // for determining the default content model 01717 'comment' => $summary, 01718 'minor_edit' => $isminor, 01719 'text' => $serialized, 01720 'len' => $newsize, 01721 'parent_id' => $oldid, 01722 'user' => $user->getId(), 01723 'user_text' => $user->getName(), 01724 'timestamp' => $now, 01725 'content_model' => $content->getModel(), 01726 'content_format' => $serialisation_format, 01727 ) ); // XXX: pass content object?! 01728 01729 $changed = !$content->equals( $old_content ); 01730 01731 if ( $changed ) { 01732 if ( !$content->isValid() ) { 01733 throw new MWException( "New content failed validity check!" ); 01734 } 01735 01736 $dbw->begin( __METHOD__ ); 01737 01738 $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); 01739 $status->merge( $prepStatus ); 01740 01741 if ( !$status->isOK() ) { 01742 $dbw->rollback( __METHOD__ ); 01743 01744 wfProfileOut( __METHOD__ ); 01745 return $status; 01746 } 01747 01748 $revisionId = $revision->insertOn( $dbw ); 01749 01750 // Update page 01751 // 01752 // Note that we use $this->mLatest instead of fetching a value from the master DB 01753 // during the course of this function. This makes sure that EditPage can detect 01754 // edit conflicts reliably, either by $ok here, or by $article->getTimestamp() 01755 // before this function is called. A previous function used a separate query, this 01756 // creates a window where concurrent edits can cause an ignored edit conflict. 01757 $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); 01758 01759 if ( !$ok ) { 01760 // Belated edit conflict! Run away!! 01761 $status->fatal( 'edit-conflict' ); 01762 01763 $dbw->rollback( __METHOD__ ); 01764 01765 wfProfileOut( __METHOD__ ); 01766 return $status; 01767 } 01768 01769 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); 01770 // Update recentchanges 01771 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01772 // Mark as patrolled if the user can do so 01773 $patrolled = $wgUseRCPatrol && !count( 01774 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01775 // Add RC row to the DB 01776 $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, 01777 $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, 01778 $revisionId, $patrolled 01779 ); 01780 01781 // Log auto-patrolled edits 01782 if ( $patrolled ) { 01783 PatrolLog::record( $rc, true, $user ); 01784 } 01785 } 01786 $user->incEditCount(); 01787 $dbw->commit( __METHOD__ ); 01788 } else { 01789 // Bug 32948: revision ID must be set to page {{REVISIONID}} and 01790 // related variables correctly 01791 $revision->setId( $this->getLatest() ); 01792 } 01793 01794 // Update links tables, site stats, etc. 01795 $this->doEditUpdates( 01796 $revision, 01797 $user, 01798 array( 01799 'changed' => $changed, 01800 'oldcountable' => $oldcountable 01801 ) 01802 ); 01803 01804 if ( !$changed ) { 01805 $status->warning( 'edit-no-change' ); 01806 $revision = null; 01807 // Update page_touched, this is usually implicit in the page update 01808 // Other cache updates are done in onArticleEdit() 01809 $this->mTitle->invalidateCache(); 01810 } 01811 } else { 01812 // Create new article 01813 $status->value['new'] = true; 01814 01815 $dbw->begin( __METHOD__ ); 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 01827 $status->merge( $prepStatus ); 01828 01829 // Add the page record; stake our claim on this title! 01830 // This will return false if the article already exists 01831 $newid = $this->insertOn( $dbw ); 01832 01833 if ( $newid === false ) { 01834 $dbw->rollback( __METHOD__ ); 01835 $status->fatal( 'edit-already-exists' ); 01836 01837 wfProfileOut( __METHOD__ ); 01838 return $status; 01839 } 01840 01841 // Save the revision text... 01842 $revision = new Revision( array( 01843 'page' => $newid, 01844 'title' => $this->getTitle(), // for determining the default content model 01845 'comment' => $summary, 01846 'minor_edit' => $isminor, 01847 'text' => $serialized, 01848 'len' => $newsize, 01849 'user' => $user->getId(), 01850 'user_text' => $user->getName(), 01851 'timestamp' => $now, 01852 'content_model' => $content->getModel(), 01853 'content_format' => $serialisation_format, 01854 ) ); 01855 $revisionId = $revision->insertOn( $dbw ); 01856 01857 // Bug 37225: use accessor to get the text as Revision may trim it 01858 $content = $revision->getContent(); // sanity; get normalized version 01859 01860 if ( $content ) { 01861 $newsize = $content->getSize(); 01862 } 01863 01864 // Update the page record with revision data 01865 $this->updateRevisionOn( $dbw, $revision, 0 ); 01866 01867 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 01868 01869 // Update recentchanges 01870 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01871 // Mark as patrolled if the user can do so 01872 $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( 01873 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01874 // Add RC row to the DB 01875 $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, 01876 '', $newsize, $revisionId, $patrolled ); 01877 01878 // Log auto-patrolled edits 01879 if ( $patrolled ) { 01880 PatrolLog::record( $rc, true, $user ); 01881 } 01882 } 01883 $user->incEditCount(); 01884 $dbw->commit( __METHOD__ ); 01885 01886 // Update links, etc. 01887 $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); 01888 01889 $hook_args = array( &$this, &$user, $content, $summary, 01890 $flags & EDIT_MINOR, null, null, &$flags, $revision ); 01891 01892 ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); 01893 wfRunHooks( 'PageContentInsertComplete', $hook_args ); 01894 } 01895 01896 // Do updates right now unless deferral was requested 01897 if ( !( $flags & EDIT_DEFER_UPDATES ) ) { 01898 DeferredUpdates::doUpdates(); 01899 } 01900 01901 // Return the new revision (or null) to the caller 01902 $status->value['revision'] = $revision; 01903 01904 $hook_args = array( &$this, &$user, $content, $summary, 01905 $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); 01906 01907 ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); 01908 wfRunHooks( 'PageContentSaveComplete', $hook_args ); 01909 01910 // Promote user to any groups they meet the criteria for 01911 $user->addAutopromoteOnceGroups( 'onEdit' ); 01912 01913 wfProfileOut( __METHOD__ ); 01914 return $status; 01915 } 01916 01931 public function makeParserOptions( $context ) { 01932 $options = $this->getContentHandler()->makeParserOptions( $context ); 01933 01934 if ( $this->getTitle()->isConversionTable() ) { 01935 //@todo: ConversionTable should become a separate content model, so we don't need special cases like this one. 01936 $options->disableContentConversion(); 01937 } 01938 01939 return $options; 01940 } 01941 01948 public function prepareTextForEdit( $text, $revid = null, User $user = null ) { 01949 ContentHandler::deprecated( __METHOD__, '1.21' ); 01950 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 01951 return $this->prepareContentForEdit( $content, $revid, $user ); 01952 } 01953 01967 public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) { 01968 global $wgContLang, $wgUser; 01969 $user = is_null( $user ) ? $wgUser : $user; 01970 //XXX: check $user->getId() here??? 01971 01972 if ( $this->mPreparedEdit 01973 && $this->mPreparedEdit->newContent 01974 && $this->mPreparedEdit->newContent->equals( $content ) 01975 && $this->mPreparedEdit->revid == $revid 01976 && $this->mPreparedEdit->format == $serialization_format 01977 // XXX: also check $user here? 01978 ) { 01979 // Already prepared 01980 return $this->mPreparedEdit; 01981 } 01982 01983 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); 01984 wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) ); 01985 01986 $edit = (object)array(); 01987 $edit->revid = $revid; 01988 01989 $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null; 01990 01991 $edit->format = $serialization_format; 01992 $edit->popts = $this->makeParserOptions( 'canonical' ); 01993 $edit->output = $edit->pstContent ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) : null; 01994 01995 $edit->newContent = $content; 01996 $edit->oldContent = $this->getContent( Revision::RAW ); 01997 01998 // NOTE: B/C for hooks! don't use these fields! 01999 $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : ''; 02000 $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; 02001 $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : ''; 02002 02003 $this->mPreparedEdit = $edit; 02004 return $edit; 02005 } 02006 02023 public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { 02024 global $wgEnableParserCache; 02025 02026 wfProfileIn( __METHOD__ ); 02027 02028 $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); 02029 $content = $revision->getContent(); 02030 02031 // Parse the text 02032 // Be careful not to double-PST: $text is usually already PST-ed once 02033 if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { 02034 wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); 02035 $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); 02036 } else { 02037 wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); 02038 $editInfo = $this->mPreparedEdit; 02039 } 02040 02041 // Save it to the parser cache 02042 if ( $wgEnableParserCache ) { 02043 $parserCache = ParserCache::singleton(); 02044 $parserCache->save( $editInfo->output, $this, $editInfo->popts ); 02045 } 02046 02047 // Update the links tables and other secondary data 02048 if ( $content ) { 02049 $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output ); 02050 DataUpdate::runUpdates( $updates ); 02051 } 02052 02053 wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); 02054 02055 if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { 02056 if ( 0 == mt_rand( 0, 99 ) ) { 02057 // Flush old entries from the `recentchanges` table; we do this on 02058 // random requests so as to avoid an increase in writes for no good reason 02059 global $wgRCMaxAge; 02060 02061 $dbw = wfGetDB( DB_MASTER ); 02062 $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); 02063 $dbw->delete( 02064 'recentchanges', 02065 array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ), 02066 __METHOD__ 02067 ); 02068 } 02069 } 02070 02071 if ( !$this->mTitle->exists() ) { 02072 wfProfileOut( __METHOD__ ); 02073 return; 02074 } 02075 02076 $id = $this->getId(); 02077 $title = $this->mTitle->getPrefixedDBkey(); 02078 $shortTitle = $this->mTitle->getDBkey(); 02079 02080 if ( !$options['changed'] ) { 02081 $good = 0; 02082 $total = 0; 02083 } elseif ( $options['created'] ) { 02084 $good = (int)$this->isCountable( $editInfo ); 02085 $total = 1; 02086 } elseif ( $options['oldcountable'] !== null ) { 02087 $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; 02088 $total = 0; 02089 } else { 02090 $good = 0; 02091 $total = 0; 02092 } 02093 02094 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); 02095 DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) ); 02096 // @TODO: let the search engine decide what to do with the content object 02097 02098 // If this is another user's talk page, update newtalk. 02099 // Don't do this if $options['changed'] = false (null-edits) nor if 02100 // it's a minor edit and the user doesn't want notifications for those. 02101 if ( $options['changed'] 02102 && $this->mTitle->getNamespace() == NS_USER_TALK 02103 && $shortTitle != $user->getTitleKey() 02104 && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) 02105 ) { 02106 if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) { 02107 $other = User::newFromName( $shortTitle, false ); 02108 if ( !$other ) { 02109 wfDebug( __METHOD__ . ": invalid username\n" ); 02110 } elseif ( User::isIP( $shortTitle ) ) { 02111 // An anonymous user 02112 $other->setNewtalk( true, $revision ); 02113 } elseif ( $other->isLoggedIn() ) { 02114 $other->setNewtalk( true, $revision ); 02115 } else { 02116 wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); 02117 } 02118 } 02119 } 02120 02121 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 02122 // XXX: could skip pseudo-messages like js/css here, based on content model. 02123 $msgtext = $content ? $content->getWikitextForTransclusion() : null; 02124 if ( $msgtext === false || $msgtext === null ) $msgtext = ''; 02125 02126 MessageCache::singleton()->replace( $shortTitle, $msgtext ); 02127 } 02128 02129 if( $options['created'] ) { 02130 self::onArticleCreate( $this->mTitle ); 02131 } else { 02132 self::onArticleEdit( $this->mTitle ); 02133 } 02134 02135 wfProfileOut( __METHOD__ ); 02136 } 02137 02150 public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { 02151 ContentHandler::deprecated( __METHOD__, "1.21" ); 02152 02153 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 02154 return $this->doQuickEditContent( $content, $user, $comment, $minor ); 02155 } 02156 02168 public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) { 02169 wfProfileIn( __METHOD__ ); 02170 02171 $serialized = $content->serialize( $serialisation_format ); 02172 02173 $dbw = wfGetDB( DB_MASTER ); 02174 $revision = new Revision( array( 02175 'title' => $this->getTitle(), // for determining the default content model 02176 'page' => $this->getId(), 02177 'text' => $serialized, 02178 'length' => $content->getSize(), 02179 'comment' => $comment, 02180 'minor_edit' => $minor ? 1 : 0, 02181 ) ); // XXX: set the content object? 02182 $revision->insertOn( $dbw ); 02183 $this->updateRevisionOn( $dbw, $revision ); 02184 02185 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 02186 02187 wfProfileOut( __METHOD__ ); 02188 } 02189 02201 public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) { 02202 global $wgContLang; 02203 02204 if ( wfReadOnly() ) { 02205 return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); 02206 } 02207 02208 $restrictionTypes = $this->mTitle->getRestrictionTypes(); 02209 02210 $id = $this->mTitle->getArticleID(); 02211 02212 if ( !$cascade ) { 02213 $cascade = false; 02214 } 02215 02216 // Take this opportunity to purge out expired restrictions 02217 Title::purgeExpiredRestrictions(); 02218 02219 // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); 02220 // we expect a single selection, but the schema allows otherwise. 02221 $isProtected = false; 02222 $protect = false; 02223 $changed = false; 02224 02225 $dbw = wfGetDB( DB_MASTER ); 02226 02227 foreach ( $restrictionTypes as $action ) { 02228 if ( !isset( $expiry[$action] ) ) { 02229 $expiry[$action] = $dbw->getInfinity(); 02230 } 02231 if ( !isset( $limit[$action] ) ) { 02232 $limit[$action] = ''; 02233 } elseif ( $limit[$action] != '' ) { 02234 $protect = true; 02235 } 02236 02237 // Get current restrictions on $action 02238 $current = implode( '', $this->mTitle->getRestrictions( $action ) ); 02239 if ( $current != '' ) { 02240 $isProtected = true; 02241 } 02242 02243 if ( $limit[$action] != $current ) { 02244 $changed = true; 02245 } elseif ( $limit[$action] != '' ) { 02246 // Only check expiry change if the action is actually being 02247 // protected, since expiry does nothing on an not-protected 02248 // action. 02249 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { 02250 $changed = true; 02251 } 02252 } 02253 } 02254 02255 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) { 02256 $changed = true; 02257 } 02258 02259 // If nothing's changed, do nothing 02260 if ( !$changed ) { 02261 return Status::newGood(); 02262 } 02263 02264 if ( !$protect ) { // No protection at all means unprotection 02265 $revCommentMsg = 'unprotectedarticle'; 02266 $logAction = 'unprotect'; 02267 } elseif ( $isProtected ) { 02268 $revCommentMsg = 'modifiedarticleprotection'; 02269 $logAction = 'modify'; 02270 } else { 02271 $revCommentMsg = 'protectedarticle'; 02272 $logAction = 'protect'; 02273 } 02274 02275 $encodedExpiry = array(); 02276 $protectDescription = ''; 02277 # Some bots may parse IRC lines, which are generated from log entries which contain plain 02278 # protect description text. Keep them in old format to avoid breaking compatibility. 02279 # TODO: Fix protection log to store structured description and format it on-the-fly. 02280 $protectDescriptionLog = ''; 02281 foreach ( $limit as $action => $restrictions ) { 02282 $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] ); 02283 if ( $restrictions != '' ) { 02284 $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ("; 02285 # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ). 02286 # All possible message keys are listed here for easier grepping: 02287 # * restriction-create 02288 # * restriction-edit 02289 # * restriction-move 02290 # * restriction-upload 02291 $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text(); 02292 # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ), 02293 # with '' filtered out. All possible message keys are listed below: 02294 # * protect-level-autoconfirmed 02295 # * protect-level-sysop 02296 $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text(); 02297 if ( $encodedExpiry[$action] != 'infinity' ) { 02298 $expiryText = wfMessage( 02299 'protect-expiring', 02300 $wgContLang->timeanddate( $expiry[$action], false, false ), 02301 $wgContLang->date( $expiry[$action], false, false ), 02302 $wgContLang->time( $expiry[$action], false, false ) 02303 )->inContentLanguage()->text(); 02304 } else { 02305 $expiryText = wfMessage( 'protect-expiry-indefinite' ) 02306 ->inContentLanguage()->text(); 02307 } 02308 02309 if ( $protectDescription !== '' ) { 02310 $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02311 } 02312 $protectDescription .= wfMessage( 'protect-summary-desc' ) 02313 ->params( $actionText, $restrictionsText, $expiryText ) 02314 ->inContentLanguage()->text(); 02315 $protectDescriptionLog .= $expiryText . ') '; 02316 } 02317 } 02318 $protectDescriptionLog = trim( $protectDescriptionLog ); 02319 02320 if ( $id ) { // Protection of existing page 02321 if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { 02322 return Status::newGood(); 02323 } 02324 02325 // Only restrictions with the 'protect' right can cascade... 02326 // Otherwise, people who cannot normally protect can "protect" pages via transclusion 02327 $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); 02328 02329 // The schema allows multiple restrictions 02330 if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) { 02331 $cascade = false; 02332 } 02333 02334 // Update restrictions table 02335 foreach ( $limit as $action => $restrictions ) { 02336 if ( $restrictions != '' ) { 02337 $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ), 02338 array( 'pr_page' => $id, 02339 'pr_type' => $action, 02340 'pr_level' => $restrictions, 02341 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, 02342 'pr_expiry' => $encodedExpiry[$action] 02343 ), 02344 __METHOD__ 02345 ); 02346 } else { 02347 $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 02348 'pr_type' => $action ), __METHOD__ ); 02349 } 02350 } 02351 02352 // Prepare a null revision to be added to the history 02353 $editComment = $wgContLang->ucfirst( 02354 wfMessage( 02355 $revCommentMsg, 02356 $this->mTitle->getPrefixedText() 02357 )->inContentLanguage()->text() 02358 ); 02359 if ( $reason ) { 02360 $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 02361 } 02362 if ( $protectDescription ) { 02363 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02364 $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )->inContentLanguage()->text(); 02365 } 02366 if ( $cascade ) { 02367 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02368 $editComment .= wfMessage( 'brackets' )->params( 02369 wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text() 02370 )->inContentLanguage()->text(); 02371 } 02372 02373 // Insert a null revision 02374 $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true ); 02375 $nullRevId = $nullRevision->insertOn( $dbw ); 02376 02377 $latest = $this->getLatest(); 02378 // Update page record 02379 $dbw->update( 'page', 02380 array( /* SET */ 02381 'page_touched' => $dbw->timestamp(), 02382 'page_restrictions' => '', 02383 'page_latest' => $nullRevId 02384 ), array( /* WHERE */ 02385 'page_id' => $id 02386 ), __METHOD__ 02387 ); 02388 02389 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); 02390 wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); 02391 } else { // Protection of non-existing page (also known as "title protection") 02392 // Cascade protection is meaningless in this case 02393 $cascade = false; 02394 02395 if ( $limit['create'] != '' ) { 02396 $dbw->replace( 'protected_titles', 02397 array( array( 'pt_namespace', 'pt_title' ) ), 02398 array( 02399 'pt_namespace' => $this->mTitle->getNamespace(), 02400 'pt_title' => $this->mTitle->getDBkey(), 02401 'pt_create_perm' => $limit['create'], 02402 'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ), 02403 'pt_expiry' => $encodedExpiry['create'], 02404 'pt_user' => $user->getId(), 02405 'pt_reason' => $reason, 02406 ), __METHOD__ 02407 ); 02408 } else { 02409 $dbw->delete( 'protected_titles', 02410 array( 02411 'pt_namespace' => $this->mTitle->getNamespace(), 02412 'pt_title' => $this->mTitle->getDBkey() 02413 ), __METHOD__ 02414 ); 02415 } 02416 } 02417 02418 $this->mTitle->flushRestrictions(); 02419 02420 if ( $logAction == 'unprotect' ) { 02421 $logParams = array(); 02422 } else { 02423 $logParams = array( $protectDescriptionLog, $cascade ? 'cascade' : '' ); 02424 } 02425 02426 // Update the protection log 02427 $log = new LogPage( 'protect' ); 02428 $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user ); 02429 02430 return Status::newGood(); 02431 } 02432 02440 protected static function flattenRestrictions( $limit ) { 02441 if ( !is_array( $limit ) ) { 02442 throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' ); 02443 } 02444 02445 $bits = array(); 02446 ksort( $limit ); 02447 02448 foreach ( $limit as $action => $restrictions ) { 02449 if ( $restrictions != '' ) { 02450 $bits[] = "$action=$restrictions"; 02451 } 02452 } 02453 02454 return implode( ':', $bits ); 02455 } 02456 02473 public function doDeleteArticle( 02474 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02475 ) { 02476 $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ); 02477 return $status->isGood(); 02478 } 02479 02497 public function doDeleteArticleReal( 02498 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02499 ) { 02500 global $wgUser, $wgContentHandlerUseDB; 02501 02502 wfDebug( __METHOD__ . "\n" ); 02503 02504 $status = Status::newGood(); 02505 02506 if ( $this->mTitle->getDBkey() === '' ) { 02507 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02508 return $status; 02509 } 02510 02511 $user = is_null( $user ) ? $wgUser : $user; 02512 if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) { 02513 if ( $status->isOK() ) { 02514 // Hook aborted but didn't set a fatal status 02515 $status->fatal( 'delete-hook-aborted' ); 02516 } 02517 return $status; 02518 } 02519 02520 if ( $id == 0 ) { 02521 $this->loadPageData( 'forupdate' ); 02522 $id = $this->getID(); 02523 if ( $id == 0 ) { 02524 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02525 return $status; 02526 } 02527 } 02528 02529 // Bitfields to further suppress the content 02530 if ( $suppress ) { 02531 $bitfield = 0; 02532 // This should be 15... 02533 $bitfield |= Revision::DELETED_TEXT; 02534 $bitfield |= Revision::DELETED_COMMENT; 02535 $bitfield |= Revision::DELETED_USER; 02536 $bitfield |= Revision::DELETED_RESTRICTED; 02537 } else { 02538 $bitfield = 'rev_deleted'; 02539 } 02540 02541 // we need to remember the old content so we can use it to generate all deletion updates. 02542 $content = $this->getContent( Revision::RAW ); 02543 02544 $dbw = wfGetDB( DB_MASTER ); 02545 $dbw->begin( __METHOD__ ); 02546 // For now, shunt the revision data into the archive table. 02547 // Text is *not* removed from the text table; bulk storage 02548 // is left intact to avoid breaking block-compression or 02549 // immutable storage schemes. 02550 // 02551 // For backwards compatibility, note that some older archive 02552 // table entries will have ar_text and ar_flags fields still. 02553 // 02554 // In the future, we may keep revisions and mark them with 02555 // the rev_deleted field, which is reserved for this purpose. 02556 02557 $row = array( 02558 'ar_namespace' => 'page_namespace', 02559 'ar_title' => 'page_title', 02560 'ar_comment' => 'rev_comment', 02561 'ar_user' => 'rev_user', 02562 'ar_user_text' => 'rev_user_text', 02563 'ar_timestamp' => 'rev_timestamp', 02564 'ar_minor_edit' => 'rev_minor_edit', 02565 'ar_rev_id' => 'rev_id', 02566 'ar_parent_id' => 'rev_parent_id', 02567 'ar_text_id' => 'rev_text_id', 02568 'ar_text' => '\'\'', // Be explicit to appease 02569 'ar_flags' => '\'\'', // MySQL's "strict mode"... 02570 'ar_len' => 'rev_len', 02571 'ar_page_id' => 'page_id', 02572 'ar_deleted' => $bitfield, 02573 'ar_sha1' => 'rev_sha1', 02574 ); 02575 02576 if ( $wgContentHandlerUseDB ) { 02577 $row[ 'ar_content_model' ] = 'rev_content_model'; 02578 $row[ 'ar_content_format' ] = 'rev_content_format'; 02579 } 02580 02581 $dbw->insertSelect( 'archive', array( 'page', 'revision' ), 02582 $row, 02583 array( 02584 'page_id' => $id, 02585 'page_id = rev_page' 02586 ), __METHOD__ 02587 ); 02588 02589 // Now that it's safely backed up, delete it 02590 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); 02591 $ok = ( $dbw->affectedRows() > 0 ); // getArticleID() uses slave, could be laggy 02592 02593 if ( !$ok ) { 02594 $dbw->rollback( __METHOD__ ); 02595 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02596 return $status; 02597 } 02598 02599 $this->doDeleteUpdates( $id, $content ); 02600 02601 // Log the deletion, if the page was suppressed, log it at Oversight instead 02602 $logtype = $suppress ? 'suppress' : 'delete'; 02603 02604 $logEntry = new ManualLogEntry( $logtype, 'delete' ); 02605 $logEntry->setPerformer( $user ); 02606 $logEntry->setTarget( $this->mTitle ); 02607 $logEntry->setComment( $reason ); 02608 $logid = $logEntry->insert(); 02609 $logEntry->publish( $logid ); 02610 02611 if ( $commit ) { 02612 $dbw->commit( __METHOD__ ); 02613 } 02614 02615 wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); 02616 $status->value = $logid; 02617 return $status; 02618 } 02619 02627 public function doDeleteUpdates( $id, Content $content = null ) { 02628 // update site status 02629 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); 02630 02631 // remove secondary indexes, etc 02632 $updates = $this->getDeletionUpdates( $content ); 02633 DataUpdate::runUpdates( $updates ); 02634 02635 // Clear caches 02636 WikiPage::onArticleDelete( $this->mTitle ); 02637 02638 // Reset this object 02639 $this->clear(); 02640 02641 // Clear the cached article id so the interface doesn't act like we exist 02642 $this->mTitle->resetArticleID( 0 ); 02643 } 02644 02669 public function doRollback( 02670 $fromP, $summary, $token, $bot, &$resultDetails, User $user 02671 ) { 02672 $resultDetails = null; 02673 02674 // Check permissions 02675 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); 02676 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); 02677 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); 02678 02679 if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { 02680 $errors[] = array( 'sessionfailure' ); 02681 } 02682 02683 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) { 02684 $errors[] = array( 'actionthrottledtext' ); 02685 } 02686 02687 // If there were errors, bail out now 02688 if ( !empty( $errors ) ) { 02689 return $errors; 02690 } 02691 02692 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user ); 02693 } 02694 02711 public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { 02712 global $wgUseRCPatrol, $wgContLang; 02713 02714 $dbw = wfGetDB( DB_MASTER ); 02715 02716 if ( wfReadOnly() ) { 02717 return array( array( 'readonlytext' ) ); 02718 } 02719 02720 // Get the last editor 02721 $current = $this->getRevision(); 02722 if ( is_null( $current ) ) { 02723 // Something wrong... no page? 02724 return array( array( 'notanarticle' ) ); 02725 } 02726 02727 $from = str_replace( '_', ' ', $fromP ); 02728 // User name given should match up with the top revision. 02729 // If the user was deleted then $from should be empty. 02730 if ( $from != $current->getUserText() ) { 02731 $resultDetails = array( 'current' => $current ); 02732 return array( array( 'alreadyrolled', 02733 htmlspecialchars( $this->mTitle->getPrefixedText() ), 02734 htmlspecialchars( $fromP ), 02735 htmlspecialchars( $current->getUserText() ) 02736 ) ); 02737 } 02738 02739 // Get the last edit not by this guy... 02740 // Note: these may not be public values 02741 $user = intval( $current->getRawUser() ); 02742 $user_text = $dbw->addQuotes( $current->getRawUserText() ); 02743 $s = $dbw->selectRow( 'revision', 02744 array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), 02745 array( 'rev_page' => $current->getPage(), 02746 "rev_user != {$user} OR rev_user_text != {$user_text}" 02747 ), __METHOD__, 02748 array( 'USE INDEX' => 'page_timestamp', 02749 'ORDER BY' => 'rev_timestamp DESC' ) 02750 ); 02751 if ( $s === false ) { 02752 // No one else ever edited this page 02753 return array( array( 'cantrollback' ) ); 02754 } elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) { 02755 // Only admins can see this text 02756 return array( array( 'notvisiblerev' ) ); 02757 } 02758 02759 $set = array(); 02760 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { 02761 // Mark all reverted edits as bot 02762 $set['rc_bot'] = 1; 02763 } 02764 02765 if ( $wgUseRCPatrol ) { 02766 // Mark all reverted edits as patrolled 02767 $set['rc_patrolled'] = 1; 02768 } 02769 02770 if ( count( $set ) ) { 02771 $dbw->update( 'recentchanges', $set, 02772 array( /* WHERE */ 02773 'rc_cur_id' => $current->getPage(), 02774 'rc_user_text' => $current->getUserText(), 02775 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), 02776 ), __METHOD__ 02777 ); 02778 } 02779 02780 // Generate the edit summary if necessary 02781 $target = Revision::newFromId( $s->rev_id ); 02782 if ( empty( $summary ) ) { 02783 if ( $from == '' ) { // no public user name 02784 $summary = wfMessage( 'revertpage-nouser' ); 02785 } else { 02786 $summary = wfMessage( 'revertpage' ); 02787 } 02788 } 02789 02790 // Allow the custom summary to use the same args as the default message 02791 $args = array( 02792 $target->getUserText(), $from, $s->rev_id, 02793 $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), 02794 $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) 02795 ); 02796 if( $summary instanceof Message ) { 02797 $summary = $summary->params( $args )->inContentLanguage()->text(); 02798 } else { 02799 $summary = wfMsgReplaceArgs( $summary, $args ); 02800 } 02801 02802 // Trim spaces on user supplied text 02803 $summary = trim( $summary ); 02804 02805 // Truncate for whole multibyte characters. 02806 $summary = $wgContLang->truncate( $summary, 255 ); 02807 02808 // Save 02809 $flags = EDIT_UPDATE; 02810 02811 if ( $guser->isAllowed( 'minoredit' ) ) { 02812 $flags |= EDIT_MINOR; 02813 } 02814 02815 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { 02816 $flags |= EDIT_FORCE_BOT; 02817 } 02818 02819 // Actually store the edit 02820 $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser ); 02821 02822 if ( !$status->isOK() ) { 02823 return $status->getErrorsArray(); 02824 } 02825 02826 if ( !empty( $status->value['revision'] ) ) { 02827 $revId = $status->value['revision']->getId(); 02828 } else { 02829 $revId = false; 02830 } 02831 02832 wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) ); 02833 02834 $resultDetails = array( 02835 'summary' => $summary, 02836 'current' => $current, 02837 'target' => $target, 02838 'newid' => $revId 02839 ); 02840 02841 return array(); 02842 } 02843 02855 public static function onArticleCreate( $title ) { 02856 // Update existence markers on article/talk tabs... 02857 if ( $title->isTalkPage() ) { 02858 $other = $title->getSubjectPage(); 02859 } else { 02860 $other = $title->getTalkPage(); 02861 } 02862 02863 $other->invalidateCache(); 02864 $other->purgeSquid(); 02865 02866 $title->touchLinks(); 02867 $title->purgeSquid(); 02868 $title->deleteTitleProtection(); 02869 } 02870 02876 public static function onArticleDelete( $title ) { 02877 // Update existence markers on article/talk tabs... 02878 if ( $title->isTalkPage() ) { 02879 $other = $title->getSubjectPage(); 02880 } else { 02881 $other = $title->getTalkPage(); 02882 } 02883 02884 $other->invalidateCache(); 02885 $other->purgeSquid(); 02886 02887 $title->touchLinks(); 02888 $title->purgeSquid(); 02889 02890 // File cache 02891 HTMLFileCache::clearFileCache( $title ); 02892 02893 // Messages 02894 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 02895 MessageCache::singleton()->replace( $title->getDBkey(), false ); 02896 } 02897 02898 // Images 02899 if ( $title->getNamespace() == NS_FILE ) { 02900 $update = new HTMLCacheUpdate( $title, 'imagelinks' ); 02901 $update->doUpdate(); 02902 } 02903 02904 // User talk pages 02905 if ( $title->getNamespace() == NS_USER_TALK ) { 02906 $user = User::newFromName( $title->getText(), false ); 02907 if ( $user ) { 02908 $user->setNewtalk( false ); 02909 } 02910 } 02911 02912 // Image redirects 02913 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); 02914 } 02915 02922 public static function onArticleEdit( $title ) { 02923 // Invalidate caches of articles which include this page 02924 DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); 02925 02926 // Invalidate the caches of all pages which redirect here 02927 DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); 02928 02929 // Purge squid for this page only 02930 $title->purgeSquid(); 02931 02932 // Clear file cache for this page only 02933 HTMLFileCache::clearFileCache( $title ); 02934 } 02935 02944 public function getHiddenCategories() { 02945 $result = array(); 02946 $id = $this->mTitle->getArticleID(); 02947 02948 if ( $id == 0 ) { 02949 return array(); 02950 } 02951 02952 $dbr = wfGetDB( DB_SLAVE ); 02953 $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), 02954 array( 'cl_to' ), 02955 array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', 02956 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), 02957 __METHOD__ ); 02958 02959 if ( $res !== false ) { 02960 foreach ( $res as $row ) { 02961 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); 02962 } 02963 } 02964 02965 return $result; 02966 } 02967 02977 public static function getAutosummary( $oldtext, $newtext, $flags ) { 02978 // NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't. 02979 02980 ContentHandler::deprecated( __METHOD__, '1.21' ); 02981 02982 $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); 02983 $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext ); 02984 $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext ); 02985 02986 return $handler->getAutosummary( $oldContent, $newContent, $flags ); 02987 } 02988 02996 public function getAutoDeleteReason( &$hasHistory ) { 02997 return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory ); 02998 } 02999 03007 public function updateCategoryCounts( $added, $deleted ) { 03008 $ns = $this->mTitle->getNamespace(); 03009 $dbw = wfGetDB( DB_MASTER ); 03010 03011 // First make sure the rows exist. If one of the "deleted" ones didn't 03012 // exist, we might legitimately not create it, but it's simpler to just 03013 // create it and then give it a negative value, since the value is bogus 03014 // anyway. 03015 // 03016 // Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. 03017 $insertCats = array_merge( $added, $deleted ); 03018 if ( !$insertCats ) { 03019 // Okay, nothing to do 03020 return; 03021 } 03022 03023 $insertRows = array(); 03024 03025 foreach ( $insertCats as $cat ) { 03026 $insertRows[] = array( 03027 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ), 03028 'cat_title' => $cat 03029 ); 03030 } 03031 $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); 03032 03033 $addFields = array( 'cat_pages = cat_pages + 1' ); 03034 $removeFields = array( 'cat_pages = cat_pages - 1' ); 03035 03036 if ( $ns == NS_CATEGORY ) { 03037 $addFields[] = 'cat_subcats = cat_subcats + 1'; 03038 $removeFields[] = 'cat_subcats = cat_subcats - 1'; 03039 } elseif ( $ns == NS_FILE ) { 03040 $addFields[] = 'cat_files = cat_files + 1'; 03041 $removeFields[] = 'cat_files = cat_files - 1'; 03042 } 03043 03044 if ( $added ) { 03045 $dbw->update( 03046 'category', 03047 $addFields, 03048 array( 'cat_title' => $added ), 03049 __METHOD__ 03050 ); 03051 } 03052 03053 if ( $deleted ) { 03054 $dbw->update( 03055 'category', 03056 $removeFields, 03057 array( 'cat_title' => $deleted ), 03058 __METHOD__ 03059 ); 03060 } 03061 } 03062 03068 public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) { 03069 if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { 03070 return; 03071 } 03072 03073 // templatelinks table may have become out of sync, 03074 // especially if using variable-based transclusions. 03075 // For paranoia, check if things have changed and if 03076 // so apply updates to the database. This will ensure 03077 // that cascaded protections apply as soon as the changes 03078 // are visible. 03079 03080 // Get templates from templatelinks 03081 $id = $this->mTitle->getArticleID(); 03082 03083 $tlTemplates = array(); 03084 03085 $dbr = wfGetDB( DB_SLAVE ); 03086 $res = $dbr->select( array( 'templatelinks' ), 03087 array( 'tl_namespace', 'tl_title' ), 03088 array( 'tl_from' => $id ), 03089 __METHOD__ 03090 ); 03091 03092 foreach ( $res as $row ) { 03093 $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; 03094 } 03095 03096 // Get templates from parser output. 03097 $poTemplates = array(); 03098 foreach ( $parserOutput->getTemplates() as $ns => $templates ) { 03099 foreach ( $templates as $dbk => $id ) { 03100 $poTemplates["$ns:$dbk"] = true; 03101 } 03102 } 03103 03104 // Get the diff 03105 $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); 03106 03107 if ( count( $templates_diff ) > 0 ) { 03108 // Whee, link updates time. 03109 // Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output. 03110 $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); 03111 $u->doUpdate(); 03112 } 03113 } 03114 03122 public function getUsedTemplates() { 03123 return $this->mTitle->getTemplateLinksFrom(); 03124 } 03125 03136 public function createUpdates( $rev ) { 03137 wfDeprecated( __METHOD__, '1.18' ); 03138 global $wgUser; 03139 $this->doEditUpdates( $rev, $wgUser, array( 'created' => true ) ); 03140 } 03141 03154 public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) { 03155 global $wgParser, $wgUser; 03156 03157 wfDeprecated( __METHOD__, '1.19' ); 03158 03159 $user = is_null( $user ) ? $wgUser : $user; 03160 03161 if ( $popts === null ) { 03162 $popts = ParserOptions::newFromUser( $user ); 03163 } 03164 03165 return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); 03166 } 03167 03174 public function isBigDeletion() { 03175 wfDeprecated( __METHOD__, '1.19' ); 03176 return $this->mTitle->isBigDeletion(); 03177 } 03178 03185 public function estimateRevisionCount() { 03186 wfDeprecated( __METHOD__, '1.19' ); 03187 return $this->mTitle->estimateRevisionCount(); 03188 } 03189 03201 public function updateRestrictions( 03202 $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null 03203 ) { 03204 global $wgUser; 03205 03206 $user = is_null( $user ) ? $wgUser : $user; 03207 03208 return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK(); 03209 } 03210 03214 public function quickEdit( $text, $comment = '', $minor = 0 ) { 03215 wfDeprecated( __METHOD__, '1.18' ); 03216 global $wgUser; 03217 $this->doQuickEdit( $text, $wgUser, $comment, $minor ); 03218 } 03219 03223 public function viewUpdates() { 03224 wfDeprecated( __METHOD__, '1.18' ); 03225 global $wgUser; 03226 return $this->doViewUpdates( $wgUser ); 03227 } 03228 03234 public function useParserCache( $oldid ) { 03235 wfDeprecated( __METHOD__, '1.18' ); 03236 global $wgUser; 03237 return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid ); 03238 } 03239 03247 public function getDeletionUpdates( Content $content = null ) { 03248 if ( !$content ) { 03249 // load content object, which may be used to determine the necessary updates 03250 // XXX: the content may not be needed to determine the updates, then this would be overhead. 03251 $content = $this->getContent( Revision::RAW ); 03252 } 03253 03254 if ( !$content ) { 03255 $updates = array(); 03256 } else { 03257 $updates = $content->getDeletionUpdates( $this ); 03258 } 03259 03260 wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); 03261 return $updates; 03262 } 03263 03264 } 03265 03266 class PoolWorkArticleView extends PoolCounterWork { 03267 03271 private $page; 03272 03276 private $cacheKey; 03277 03281 private $revid; 03282 03286 private $parserOptions; 03287 03291 private $content = null; 03292 03296 private $parserOutput = false; 03297 03301 private $isDirty = false; 03302 03306 private $error = false; 03307 03317 function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) { 03318 if ( is_string( $content ) ) { // BC: old style call 03319 $modelId = $page->getRevision()->getContentModel(); 03320 $format = $page->getRevision()->getContentFormat(); 03321 $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format ); 03322 } 03323 03324 $this->page = $page; 03325 $this->revid = $revid; 03326 $this->cacheable = $useParserCache; 03327 $this->parserOptions = $parserOptions; 03328 $this->content = $content; 03329 $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); 03330 parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); 03331 } 03332 03338 public function getParserOutput() { 03339 return $this->parserOutput; 03340 } 03341 03347 public function getIsDirty() { 03348 return $this->isDirty; 03349 } 03350 03356 public function getError() { 03357 return $this->error; 03358 } 03359 03363 function doWork() { 03364 global $wgUseFileCache; 03365 03366 // @todo: several of the methods called on $this->page are not declared in Page, but present 03367 // in WikiPage and delegated by Article. 03368 03369 $isCurrent = $this->revid === $this->page->getLatest(); 03370 03371 if ( $this->content !== null ) { 03372 $content = $this->content; 03373 } elseif ( $isCurrent ) { 03374 // XXX: why use RAW audience here, and PUBLIC (default) below? 03375 $content = $this->page->getContent( Revision::RAW ); 03376 } else { 03377 $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); 03378 03379 if ( $rev === null ) { 03380 $content = null; 03381 } else { 03382 // XXX: why use PUBLIC audience here (default), and RAW above? 03383 $content = $rev->getContent(); 03384 } 03385 } 03386 03387 if ( $content === null ) { 03388 return false; 03389 } 03390 03391 $time = - microtime( true ); 03392 $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions ); 03393 $time += microtime( true ); 03394 03395 // Timing hack 03396 if ( $time > 3 ) { 03397 wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, 03398 $this->page->getTitle()->getPrefixedDBkey() ) ); 03399 } 03400 03401 if ( $this->cacheable && $this->parserOutput->isCacheable() ) { 03402 ParserCache::singleton()->save( $this->parserOutput, $this->page, $this->parserOptions ); 03403 } 03404 03405 // Make sure file cache is not used on uncacheable content. 03406 // Output that has magic words in it can still use the parser cache 03407 // (if enabled), though it will generally expire sooner. 03408 if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) { 03409 $wgUseFileCache = false; 03410 } 03411 03412 if ( $isCurrent ) { 03413 $this->page->doCascadeProtectionUpdates( $this->parserOutput ); 03414 } 03415 03416 return true; 03417 } 03418 03422 function getCachedWork() { 03423 $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions ); 03424 03425 if ( $this->parserOutput === false ) { 03426 wfDebug( __METHOD__ . ": parser cache miss\n" ); 03427 return false; 03428 } else { 03429 wfDebug( __METHOD__ . ": parser cache hit\n" ); 03430 return true; 03431 } 03432 } 03433 03437 function fallback() { 03438 $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions ); 03439 03440 if ( $this->parserOutput === false ) { 03441 wfDebugLog( 'dirty', "dirty missing\n" ); 03442 wfDebug( __METHOD__ . ": no dirty cache\n" ); 03443 return false; 03444 } else { 03445 wfDebug( __METHOD__ . ": sending dirty output\n" ); 03446 wfDebugLog( 'dirty', "dirty output {$this->cacheKey}\n" ); 03447 $this->isDirty = true; 03448 return true; 03449 } 03450 } 03451 03456 function error( $status ) { 03457 $this->error = $status; 03458 return false; 03459 } 03460 }