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