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 $mId = null; 00057 00061 protected $mDataLoadedFrom = self::READ_NONE; 00062 00066 protected $mRedirectTarget = null; 00067 00071 protected $mLastRevision = null; 00072 00076 protected $mTimestamp = ''; 00077 00081 protected $mTouched = '19700101000000'; 00082 00086 protected $mCounter = null; 00087 00092 public function __construct( Title $title ) { 00093 $this->mTitle = $title; 00094 } 00095 00103 public static function factory( Title $title ) { 00104 $ns = $title->getNamespace(); 00105 00106 if ( $ns == NS_MEDIA ) { 00107 throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." ); 00108 } elseif ( $ns < 0 ) { 00109 throw new MWException( "Invalid or virtual namespace $ns given." ); 00110 } 00111 00112 switch ( $ns ) { 00113 case NS_FILE: 00114 $page = new WikiFilePage( $title ); 00115 break; 00116 case NS_CATEGORY: 00117 $page = new WikiCategoryPage( $title ); 00118 break; 00119 default: 00120 $page = new WikiPage( $title ); 00121 } 00122 00123 return $page; 00124 } 00125 00136 public static function newFromID( $id, $from = 'fromdb' ) { 00137 $from = self::convertSelectType( $from ); 00138 $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE ); 00139 $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ ); 00140 if ( !$row ) { 00141 return null; 00142 } 00143 return self::newFromRow( $row, $from ); 00144 } 00145 00158 public static function newFromRow( $row, $from = 'fromdb' ) { 00159 $page = self::factory( Title::newFromRow( $row ) ); 00160 $page->loadFromRow( $row, $from ); 00161 return $page; 00162 } 00163 00170 private static function convertSelectType( $type ) { 00171 switch ( $type ) { 00172 case 'fromdb': 00173 return self::READ_NORMAL; 00174 case 'fromdbmaster': 00175 return self::READ_LATEST; 00176 case 'forupdate': 00177 return self::READ_LOCKING; 00178 default: 00179 // It may already be an integer or whatever else 00180 return $type; 00181 } 00182 } 00183 00194 public function getActionOverrides() { 00195 $content_handler = $this->getContentHandler(); 00196 return $content_handler->getActionOverrides(); 00197 } 00198 00208 public function getContentHandler() { 00209 return ContentHandler::getForModelID( $this->getContentModel() ); 00210 } 00211 00216 public function getTitle() { 00217 return $this->mTitle; 00218 } 00219 00224 public function clear() { 00225 $this->mDataLoaded = false; 00226 $this->mDataLoadedFrom = self::READ_NONE; 00227 00228 $this->clearCacheFields(); 00229 } 00230 00235 protected function clearCacheFields() { 00236 $this->mId = null; 00237 $this->mCounter = null; 00238 $this->mRedirectTarget = null; // Title object if set 00239 $this->mLastRevision = null; // Latest revision 00240 $this->mTouched = '19700101000000'; 00241 $this->mTimestamp = ''; 00242 $this->mIsRedirect = false; 00243 $this->mLatest = false; 00244 $this->mPreparedEdit = false; 00245 } 00246 00253 public static function selectFields() { 00254 global $wgContentHandlerUseDB; 00255 00256 $fields = array( 00257 'page_id', 00258 'page_namespace', 00259 'page_title', 00260 'page_restrictions', 00261 'page_counter', 00262 'page_is_redirect', 00263 'page_is_new', 00264 'page_random', 00265 'page_touched', 00266 'page_latest', 00267 'page_len', 00268 ); 00269 00270 if ( $wgContentHandlerUseDB ) { 00271 $fields[] = 'page_content_model'; 00272 } 00273 00274 return $fields; 00275 } 00276 00284 protected function pageData( $dbr, $conditions, $options = array() ) { 00285 $fields = self::selectFields(); 00286 00287 wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); 00288 00289 $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); 00290 00291 wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); 00292 00293 return $row; 00294 } 00295 00305 public function pageDataFromTitle( $dbr, $title, $options = array() ) { 00306 return $this->pageData( $dbr, array( 00307 'page_namespace' => $title->getNamespace(), 00308 'page_title' => $title->getDBkey() ), $options ); 00309 } 00310 00319 public function pageDataFromId( $dbr, $id, $options = array() ) { 00320 return $this->pageData( $dbr, array( 'page_id' => $id ), $options ); 00321 } 00322 00335 public function loadPageData( $from = 'fromdb' ) { 00336 $from = self::convertSelectType( $from ); 00337 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) { 00338 // We already have the data from the correct location, no need to load it twice. 00339 return; 00340 } 00341 00342 if ( $from === self::READ_LOCKING ) { 00343 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); 00344 } elseif ( $from === self::READ_LATEST ) { 00345 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00346 } elseif ( $from === self::READ_NORMAL ) { 00347 $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); 00348 // Use a "last rev inserted" timestamp key to diminish the issue of slave lag. 00349 // Note that DB also stores the master position in the session and checks it. 00350 $touched = $this->getCachedLastEditTime(); 00351 if ( $touched ) { // key set 00352 if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { 00353 $from = self::READ_LATEST; 00354 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00355 } 00356 } 00357 } else { 00358 // No idea from where the caller got this data, assume slave database. 00359 $data = $from; 00360 $from = self::READ_NORMAL; 00361 } 00362 00363 $this->loadFromRow( $data, $from ); 00364 } 00365 00378 public function loadFromRow( $data, $from ) { 00379 $lc = LinkCache::singleton(); 00380 $lc->clearLink( $this->mTitle ); 00381 00382 if ( $data ) { 00383 $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); 00384 00385 $this->mTitle->loadFromRow( $data ); 00386 00387 // Old-fashioned restrictions 00388 $this->mTitle->loadRestrictions( $data->page_restrictions ); 00389 00390 $this->mId = intval( $data->page_id ); 00391 $this->mCounter = intval( $data->page_counter ); 00392 $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); 00393 $this->mIsRedirect = intval( $data->page_is_redirect ); 00394 $this->mLatest = intval( $data->page_latest ); 00395 // Bug 37225: $latest may no longer match the cached latest Revision object. 00396 // Double-check the ID of any cached latest Revision object for consistency. 00397 if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { 00398 $this->mLastRevision = null; 00399 $this->mTimestamp = ''; 00400 } 00401 } else { 00402 $lc->addBadLinkObj( $this->mTitle ); 00403 00404 $this->mTitle->loadFromRow( false ); 00405 00406 $this->clearCacheFields(); 00407 00408 $this->mId = 0; 00409 } 00410 00411 $this->mDataLoaded = true; 00412 $this->mDataLoadedFrom = self::convertSelectType( $from ); 00413 } 00414 00418 public function getId() { 00419 if ( !$this->mDataLoaded ) { 00420 $this->loadPageData(); 00421 } 00422 return $this->mId; 00423 } 00424 00428 public function exists() { 00429 if ( !$this->mDataLoaded ) { 00430 $this->loadPageData(); 00431 } 00432 return $this->mId > 0; 00433 } 00434 00443 public function hasViewableContent() { 00444 return $this->exists() || $this->mTitle->isAlwaysKnown(); 00445 } 00446 00450 public function getCount() { 00451 if ( !$this->mDataLoaded ) { 00452 $this->loadPageData(); 00453 } 00454 00455 return $this->mCounter; 00456 } 00457 00463 public function isRedirect( ) { 00464 $content = $this->getContent(); 00465 if ( !$content ) return false; 00466 00467 return $content->isRedirect(); 00468 } 00469 00480 public function getContentModel() { 00481 if ( $this->exists() ) { 00482 // look at the revision's actual content model 00483 $rev = $this->getRevision(); 00484 00485 if ( $rev !== null ) { 00486 return $rev->getContentModel(); 00487 } else { 00488 $title = $this->mTitle->getPrefixedDBkey(); 00489 wfWarn( "Page $title exists but has no (visible) revisions!" ); 00490 } 00491 } 00492 00493 // use the default model for this page 00494 return $this->mTitle->getContentModel(); 00495 } 00496 00501 public function checkTouched() { 00502 if ( !$this->mDataLoaded ) { 00503 $this->loadPageData(); 00504 } 00505 return !$this->mIsRedirect; 00506 } 00507 00512 public function getTouched() { 00513 if ( !$this->mDataLoaded ) { 00514 $this->loadPageData(); 00515 } 00516 return $this->mTouched; 00517 } 00518 00523 public function getLatest() { 00524 if ( !$this->mDataLoaded ) { 00525 $this->loadPageData(); 00526 } 00527 return (int)$this->mLatest; 00528 } 00529 00534 public function getOldestRevision() { 00535 wfProfileIn( __METHOD__ ); 00536 00537 // Try using the slave database first, then try the master 00538 $continue = 2; 00539 $db = wfGetDB( DB_SLAVE ); 00540 $revSelectFields = Revision::selectFields(); 00541 00542 while ( $continue ) { 00543 $row = $db->selectRow( 00544 array( 'page', 'revision' ), 00545 $revSelectFields, 00546 array( 00547 'page_namespace' => $this->mTitle->getNamespace(), 00548 'page_title' => $this->mTitle->getDBkey(), 00549 'rev_page = page_id' 00550 ), 00551 __METHOD__, 00552 array( 00553 'ORDER BY' => 'rev_timestamp ASC' 00554 ) 00555 ); 00556 00557 if ( $row ) { 00558 $continue = 0; 00559 } else { 00560 $db = wfGetDB( DB_MASTER ); 00561 $continue--; 00562 } 00563 } 00564 00565 wfProfileOut( __METHOD__ ); 00566 return $row ? Revision::newFromRow( $row ) : null; 00567 } 00568 00573 protected function loadLastEdit() { 00574 if ( $this->mLastRevision !== null ) { 00575 return; // already loaded 00576 } 00577 00578 $latest = $this->getLatest(); 00579 if ( !$latest ) { 00580 return; // page doesn't exist or is missing page_latest info 00581 } 00582 00583 // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the 00584 // latest changes committed. This is true even within REPEATABLE-READ transactions, where 00585 // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to 00586 // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row 00587 // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. 00588 // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. 00589 $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; 00590 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); 00591 if ( $revision ) { // sanity 00592 $this->setLastEdit( $revision ); 00593 } 00594 } 00595 00599 protected function setLastEdit( Revision $revision ) { 00600 $this->mLastRevision = $revision; 00601 $this->mTimestamp = $revision->getTimestamp(); 00602 } 00603 00608 public function getRevision() { 00609 $this->loadLastEdit(); 00610 if ( $this->mLastRevision ) { 00611 return $this->mLastRevision; 00612 } 00613 return null; 00614 } 00615 00629 public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00630 $this->loadLastEdit(); 00631 if ( $this->mLastRevision ) { 00632 return $this->mLastRevision->getContent( $audience, $user ); 00633 } 00634 return null; 00635 } 00636 00649 public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { // @todo: deprecated, replace usage! 00650 ContentHandler::deprecated( __METHOD__, '1.21' ); 00651 00652 $this->loadLastEdit(); 00653 if ( $this->mLastRevision ) { 00654 return $this->mLastRevision->getText( $audience, $user ); 00655 } 00656 return false; 00657 } 00658 00665 public function getRawText() { 00666 ContentHandler::deprecated( __METHOD__, '1.21' ); 00667 00668 return $this->getText( Revision::RAW ); 00669 } 00670 00674 public function getTimestamp() { 00675 // Check if the field has been filled by WikiPage::setTimestamp() 00676 if ( !$this->mTimestamp ) { 00677 $this->loadLastEdit(); 00678 } 00679 00680 return wfTimestamp( TS_MW, $this->mTimestamp ); 00681 } 00682 00688 public function setTimestamp( $ts ) { 00689 $this->mTimestamp = wfTimestamp( TS_MW, $ts ); 00690 } 00691 00701 public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00702 $this->loadLastEdit(); 00703 if ( $this->mLastRevision ) { 00704 return $this->mLastRevision->getUser( $audience, $user ); 00705 } else { 00706 return -1; 00707 } 00708 } 00709 00720 public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00721 $revision = $this->getOldestRevision(); 00722 if ( $revision ) { 00723 $userName = $revision->getUserText( $audience, $user ); 00724 return User::newFromName( $userName, false ); 00725 } else { 00726 return null; 00727 } 00728 } 00729 00739 public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00740 $this->loadLastEdit(); 00741 if ( $this->mLastRevision ) { 00742 return $this->mLastRevision->getUserText( $audience, $user ); 00743 } else { 00744 return ''; 00745 } 00746 } 00747 00757 public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00758 $this->loadLastEdit(); 00759 if ( $this->mLastRevision ) { 00760 return $this->mLastRevision->getComment( $audience, $user ); 00761 } else { 00762 return ''; 00763 } 00764 } 00765 00771 public function getMinorEdit() { 00772 $this->loadLastEdit(); 00773 if ( $this->mLastRevision ) { 00774 return $this->mLastRevision->isMinor(); 00775 } else { 00776 return false; 00777 } 00778 } 00779 00785 protected function getCachedLastEditTime() { 00786 global $wgMemc; 00787 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00788 return $wgMemc->get( $key ); 00789 } 00790 00797 public function setCachedLastEditTime( $timestamp ) { 00798 global $wgMemc; 00799 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00800 $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60*15 ); 00801 } 00802 00811 public function isCountable( $editInfo = false ) { 00812 global $wgArticleCountMethod; 00813 00814 if ( !$this->mTitle->isContentPage() ) { 00815 return false; 00816 } 00817 00818 if ( $editInfo ) { 00819 $content = $editInfo->pstContent; 00820 } else { 00821 $content = $this->getContent(); 00822 } 00823 00824 if ( !$content || $content->isRedirect( ) ) { 00825 return false; 00826 } 00827 00828 $hasLinks = null; 00829 00830 if ( $wgArticleCountMethod === 'link' ) { 00831 // nasty special case to avoid re-parsing to detect links 00832 00833 if ( $editInfo ) { 00834 // ParserOutput::getLinks() is a 2D array of page links, so 00835 // to be really correct we would need to recurse in the array 00836 // but the main array should only have items in it if there are 00837 // links. 00838 $hasLinks = (bool)count( $editInfo->output->getLinks() ); 00839 } else { 00840 $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, 00841 array( 'pl_from' => $this->getId() ), __METHOD__ ); 00842 } 00843 } 00844 00845 return $content->isCountable( $hasLinks ); 00846 } 00847 00855 public function getRedirectTarget() { 00856 if ( !$this->mTitle->isRedirect() ) { 00857 return null; 00858 } 00859 00860 if ( $this->mRedirectTarget !== null ) { 00861 return $this->mRedirectTarget; 00862 } 00863 00864 // Query the redirect table 00865 $dbr = wfGetDB( DB_SLAVE ); 00866 $row = $dbr->selectRow( 'redirect', 00867 array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), 00868 array( 'rd_from' => $this->getId() ), 00869 __METHOD__ 00870 ); 00871 00872 // rd_fragment and rd_interwiki were added later, populate them if empty 00873 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { 00874 return $this->mRedirectTarget = Title::makeTitle( 00875 $row->rd_namespace, $row->rd_title, 00876 $row->rd_fragment, $row->rd_interwiki ); 00877 } 00878 00879 // This page doesn't have an entry in the redirect table 00880 return $this->mRedirectTarget = $this->insertRedirect(); 00881 } 00882 00889 public function insertRedirect() { 00890 // recurse through to only get the final target 00891 $content = $this->getContent(); 00892 $retval = $content ? $content->getUltimateRedirectTarget() : null; 00893 if ( !$retval ) { 00894 return null; 00895 } 00896 $this->insertRedirectEntry( $retval ); 00897 return $retval; 00898 } 00899 00905 public function insertRedirectEntry( $rt ) { 00906 $dbw = wfGetDB( DB_MASTER ); 00907 $dbw->replace( 'redirect', array( 'rd_from' ), 00908 array( 00909 'rd_from' => $this->getId(), 00910 'rd_namespace' => $rt->getNamespace(), 00911 'rd_title' => $rt->getDBkey(), 00912 'rd_fragment' => $rt->getFragment(), 00913 'rd_interwiki' => $rt->getInterwiki(), 00914 ), 00915 __METHOD__ 00916 ); 00917 } 00918 00924 public function followRedirect() { 00925 return $this->getRedirectURL( $this->getRedirectTarget() ); 00926 } 00927 00935 public function getRedirectURL( $rt ) { 00936 if ( !$rt ) { 00937 return false; 00938 } 00939 00940 if ( $rt->isExternal() ) { 00941 if ( $rt->isLocal() ) { 00942 // Offsite wikis need an HTTP redirect. 00943 // 00944 // This can be hard to reverse and may produce loops, 00945 // so they may be disabled in the site configuration. 00946 $source = $this->mTitle->getFullURL( 'redirect=no' ); 00947 return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); 00948 } else { 00949 // External pages pages without "local" bit set are not valid 00950 // redirect targets 00951 return false; 00952 } 00953 } 00954 00955 if ( $rt->isSpecialPage() ) { 00956 // Gotta handle redirects to special pages differently: 00957 // Fill the HTTP response "Location" header and ignore 00958 // the rest of the page we're on. 00959 // 00960 // Some pages are not valid targets 00961 if ( $rt->isValidRedirectTarget() ) { 00962 return $rt->getFullURL(); 00963 } else { 00964 return false; 00965 } 00966 } 00967 00968 return $rt; 00969 } 00970 00976 public function getContributors() { 00977 // @todo FIXME: This is expensive; cache this info somewhere. 00978 00979 $dbr = wfGetDB( DB_SLAVE ); 00980 00981 if ( $dbr->implicitGroupby() ) { 00982 $realNameField = 'user_real_name'; 00983 } else { 00984 $realNameField = 'MIN(user_real_name) AS user_real_name'; 00985 } 00986 00987 $tables = array( 'revision', 'user' ); 00988 00989 $fields = array( 00990 'user_id' => 'rev_user', 00991 'user_name' => 'rev_user_text', 00992 $realNameField, 00993 'timestamp' => 'MAX(rev_timestamp)', 00994 ); 00995 00996 $conds = array( 'rev_page' => $this->getId() ); 00997 00998 // The user who made the top revision gets credited as "this page was last edited by 00999 // John, based on contributions by Tom, Dick and Harry", so don't include them twice. 01000 $user = $this->getUser(); 01001 if ( $user ) { 01002 $conds[] = "rev_user != $user"; 01003 } else { 01004 $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}"; 01005 } 01006 01007 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden? 01008 01009 $jconds = array( 01010 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ), 01011 ); 01012 01013 $options = array( 01014 'GROUP BY' => array( 'rev_user', 'rev_user_text' ), 01015 'ORDER BY' => 'timestamp DESC', 01016 ); 01017 01018 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds ); 01019 return new UserArrayFromResult( $res ); 01020 } 01021 01028 public function getLastNAuthors( $num, $revLatest = 0 ) { 01029 wfProfileIn( __METHOD__ ); 01030 // First try the slave 01031 // If that doesn't have the latest revision, try the master 01032 $continue = 2; 01033 $db = wfGetDB( DB_SLAVE ); 01034 01035 do { 01036 $res = $db->select( array( 'page', 'revision' ), 01037 array( 'rev_id', 'rev_user_text' ), 01038 array( 01039 'page_namespace' => $this->mTitle->getNamespace(), 01040 'page_title' => $this->mTitle->getDBkey(), 01041 'rev_page = page_id' 01042 ), __METHOD__, 01043 array( 01044 'ORDER BY' => 'rev_timestamp DESC', 01045 'LIMIT' => $num 01046 ) 01047 ); 01048 01049 if ( !$res ) { 01050 wfProfileOut( __METHOD__ ); 01051 return array(); 01052 } 01053 01054 $row = $db->fetchObject( $res ); 01055 01056 if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { 01057 $db = wfGetDB( DB_MASTER ); 01058 $continue--; 01059 } else { 01060 $continue = 0; 01061 } 01062 } while ( $continue ); 01063 01064 $authors = array( $row->rev_user_text ); 01065 01066 foreach ( $res as $row ) { 01067 $authors[] = $row->rev_user_text; 01068 } 01069 01070 wfProfileOut( __METHOD__ ); 01071 return $authors; 01072 } 01073 01081 public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { 01082 global $wgEnableParserCache; 01083 01084 return $wgEnableParserCache 01085 && $parserOptions->getStubThreshold() == 0 01086 && $this->exists() 01087 && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) 01088 && $this->getContentHandler()->isParserCacheSupported(); 01089 } 01090 01102 public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { 01103 wfProfileIn( __METHOD__ ); 01104 01105 $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); 01106 wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); 01107 if ( $parserOptions->getStubThreshold() ) { 01108 wfIncrStats( 'pcache_miss_stub' ); 01109 } 01110 01111 if ( $useParserCache ) { 01112 $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); 01113 if ( $parserOutput !== false ) { 01114 wfProfileOut( __METHOD__ ); 01115 return $parserOutput; 01116 } 01117 } 01118 01119 if ( $oldid === null || $oldid === 0 ) { 01120 $oldid = $this->getLatest(); 01121 } 01122 01123 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache ); 01124 $pool->execute(); 01125 01126 wfProfileOut( __METHOD__ ); 01127 01128 return $pool->getParserOutput(); 01129 } 01130 01135 public function doViewUpdates( User $user ) { 01136 global $wgDisableCounters; 01137 if ( wfReadOnly() ) { 01138 return; 01139 } 01140 01141 // Don't update page view counters on views from bot users (bug 14044) 01142 if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->exists() ) { 01143 DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); 01144 DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); 01145 } 01146 01147 // Update newtalk / watchlist notification status 01148 $user->clearNotification( $this->mTitle ); 01149 } 01150 01155 public function doPurge() { 01156 global $wgUseSquid; 01157 01158 if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { 01159 return false; 01160 } 01161 01162 // Invalidate the cache 01163 $this->mTitle->invalidateCache(); 01164 $this->clear(); 01165 01166 if ( $wgUseSquid ) { 01167 // Commit the transaction before the purge is sent 01168 $dbw = wfGetDB( DB_MASTER ); 01169 $dbw->commit( __METHOD__ ); 01170 01171 // Send purge 01172 $update = SquidUpdate::newSimplePurge( $this->mTitle ); 01173 $update->doUpdate(); 01174 } 01175 01176 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 01177 // @todo: move this logic to MessageCache 01178 01179 if ( $this->exists() ) { 01180 // NOTE: use transclusion text for messages. 01181 // This is consistent with MessageCache::getMsgFromNamespace() 01182 01183 $content = $this->getContent(); 01184 $text = $content === null ? null : $content->getWikitextForTransclusion(); 01185 01186 if ( $text === null ) $text = false; 01187 } else { 01188 $text = false; 01189 } 01190 01191 MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); 01192 } 01193 return true; 01194 } 01195 01206 public function insertOn( $dbw ) { 01207 wfProfileIn( __METHOD__ ); 01208 01209 $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); 01210 $dbw->insert( 'page', array( 01211 'page_id' => $page_id, 01212 'page_namespace' => $this->mTitle->getNamespace(), 01213 'page_title' => $this->mTitle->getDBkey(), 01214 'page_counter' => 0, 01215 'page_restrictions' => '', 01216 'page_is_redirect' => 0, // Will set this shortly... 01217 'page_is_new' => 1, 01218 'page_random' => wfRandom(), 01219 'page_touched' => $dbw->timestamp(), 01220 'page_latest' => 0, // Fill this in shortly... 01221 'page_len' => 0, // Fill this in shortly... 01222 ), __METHOD__, 'IGNORE' ); 01223 01224 $affected = $dbw->affectedRows(); 01225 01226 if ( $affected ) { 01227 $newid = $dbw->insertId(); 01228 $this->mId = $newid; 01229 $this->mTitle->resetArticleID( $newid ); 01230 } 01231 wfProfileOut( __METHOD__ ); 01232 01233 return $affected ? $newid : false; 01234 } 01235 01251 public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { 01252 global $wgContentHandlerUseDB; 01253 01254 wfProfileIn( __METHOD__ ); 01255 01256 $content = $revision->getContent(); 01257 $len = $content ? $content->getSize() : 0; 01258 $rt = $content ? $content->getUltimateRedirectTarget() : null; 01259 01260 $conditions = array( 'page_id' => $this->getId() ); 01261 01262 if ( !is_null( $lastRevision ) ) { 01263 // An extra check against threads stepping on each other 01264 $conditions['page_latest'] = $lastRevision; 01265 } 01266 01267 $now = wfTimestampNow(); 01268 $row = array( /* SET */ 01269 'page_latest' => $revision->getId(), 01270 'page_touched' => $dbw->timestamp( $now ), 01271 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, 01272 'page_is_redirect' => $rt !== null ? 1 : 0, 01273 'page_len' => $len, 01274 ); 01275 01276 if ( $wgContentHandlerUseDB ) { 01277 $row[ 'page_content_model' ] = $revision->getContentModel(); 01278 } 01279 01280 $dbw->update( 'page', 01281 $row, 01282 $conditions, 01283 __METHOD__ ); 01284 01285 $result = $dbw->affectedRows() > 0; 01286 if ( $result ) { 01287 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); 01288 $this->setLastEdit( $revision ); 01289 $this->setCachedLastEditTime( $now ); 01290 $this->mLatest = $revision->getId(); 01291 $this->mIsRedirect = (bool)$rt; 01292 // Update the LinkCache. 01293 LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, 01294 $this->mLatest, $revision->getContentModel() ); 01295 } 01296 01297 wfProfileOut( __METHOD__ ); 01298 return $result; 01299 } 01300 01312 public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) { 01313 // Always update redirects (target link might have changed) 01314 // Update/Insert if we don't know if the last revision was a redirect or not 01315 // Delete if changing from redirect to non-redirect 01316 $isRedirect = !is_null( $redirectTitle ); 01317 01318 if ( !$isRedirect && $lastRevIsRedirect === false ) { 01319 return true; 01320 } 01321 01322 wfProfileIn( __METHOD__ ); 01323 if ( $isRedirect ) { 01324 $this->insertRedirectEntry( $redirectTitle ); 01325 } else { 01326 // This is not a redirect, remove row from redirect table 01327 $where = array( 'rd_from' => $this->getId() ); 01328 $dbw->delete( 'redirect', $where, __METHOD__ ); 01329 } 01330 01331 if ( $this->getTitle()->getNamespace() == NS_FILE ) { 01332 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); 01333 } 01334 wfProfileOut( __METHOD__ ); 01335 01336 return ( $dbw->affectedRows() != 0 ); 01337 } 01338 01347 public function updateIfNewerOn( $dbw, $revision ) { 01348 wfProfileIn( __METHOD__ ); 01349 01350 $row = $dbw->selectRow( 01351 array( 'revision', 'page' ), 01352 array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), 01353 array( 01354 'page_id' => $this->getId(), 01355 'page_latest=rev_id' ), 01356 __METHOD__ ); 01357 01358 if ( $row ) { 01359 if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { 01360 wfProfileOut( __METHOD__ ); 01361 return false; 01362 } 01363 $prev = $row->rev_id; 01364 $lastRevIsRedirect = (bool)$row->page_is_redirect; 01365 } else { 01366 // No or missing previous revision; mark the page as new 01367 $prev = 0; 01368 $lastRevIsRedirect = null; 01369 } 01370 01371 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); 01372 01373 wfProfileOut( __METHOD__ ); 01374 return $ret; 01375 } 01376 01387 public function getUndoContent( Revision $undo, Revision $undoafter = null ) { 01388 $handler = $undo->getContentHandler(); 01389 return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter ); 01390 } 01391 01401 public function getUndoText( Revision $undo, Revision $undoafter = null ) { 01402 ContentHandler::deprecated( __METHOD__, '1.21' ); 01403 01404 $this->loadLastEdit(); 01405 01406 if ( $this->mLastRevision ) { 01407 if ( is_null( $undoafter ) ) { 01408 $undoafter = $undo->getPrevious(); 01409 } 01410 01411 $handler = $this->getContentHandler(); 01412 $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); 01413 01414 if ( !$undone ) { 01415 return false; 01416 } else { 01417 return ContentHandler::getContentText( $undone ); 01418 } 01419 } 01420 01421 return false; 01422 } 01423 01434 public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { 01435 ContentHandler::deprecated( __METHOD__, '1.21' ); 01436 01437 if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent! 01438 // Whole-page edit; let the whole text through 01439 return $text; 01440 } 01441 01442 if ( !$this->supportsSections() ) { 01443 throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); 01444 } 01445 01446 // could even make section title, but that's not required. 01447 $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); 01448 01449 $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime ); 01450 01451 return ContentHandler::getContentText( $newContent ); 01452 } 01453 01462 public function supportsSections() { 01463 return $this->getContentHandler()->supportsSections(); 01464 } 01465 01477 public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) { 01478 wfProfileIn( __METHOD__ ); 01479 01480 if ( strval( $section ) == '' ) { 01481 // Whole-page edit; let the whole text through 01482 $newContent = $sectionContent; 01483 } else { 01484 if ( !$this->supportsSections() ) { 01485 throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() ); 01486 } 01487 01488 // Bug 30711: always use current version when adding a new section 01489 if ( is_null( $edittime ) || $section == 'new' ) { 01490 $oldContent = $this->getContent(); 01491 } else { 01492 $dbw = wfGetDB( DB_MASTER ); 01493 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); 01494 01495 if ( !$rev ) { 01496 wfDebug( "WikiPage::replaceSection asked for bogus section (page: " . 01497 $this->getId() . "; section: $section; edittime: $edittime)\n" ); 01498 wfProfileOut( __METHOD__ ); 01499 return null; 01500 } 01501 01502 $oldContent = $rev->getContent(); 01503 } 01504 01505 if ( ! $oldContent ) { 01506 wfDebug( __METHOD__ . ": no page text\n" ); 01507 wfProfileOut( __METHOD__ ); 01508 return null; 01509 } 01510 01511 // FIXME: $oldContent might be null? 01512 $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle ); 01513 } 01514 01515 wfProfileOut( __METHOD__ ); 01516 return $newContent; 01517 } 01518 01524 function checkFlags( $flags ) { 01525 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { 01526 if ( $this->exists() ) { 01527 $flags |= EDIT_UPDATE; 01528 } else { 01529 $flags |= EDIT_NEW; 01530 } 01531 } 01532 01533 return $flags; 01534 } 01535 01585 public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { 01586 ContentHandler::deprecated( __METHOD__, '1.21' ); 01587 01588 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 01589 01590 return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); 01591 } 01592 01641 public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, 01642 User $user = null, $serialisation_format = null ) { 01643 global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; 01644 01645 // Low-level sanity check 01646 if ( $this->mTitle->getText() === '' ) { 01647 throw new MWException( 'Something is trying to edit an article with an empty title' ); 01648 } 01649 01650 wfProfileIn( __METHOD__ ); 01651 01652 if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { 01653 wfProfileOut( __METHOD__ ); 01654 return Status::newFatal( 'content-not-allowed-here', 01655 ContentHandler::getLocalizedName( $content->getModel() ), 01656 $this->getTitle()->getPrefixedText() ); 01657 } 01658 01659 $user = is_null( $user ) ? $wgUser : $user; 01660 $status = Status::newGood( array() ); 01661 01662 // Load the data from the master database if needed. 01663 // The caller may already loaded it from the master or even loaded it using 01664 // SELECT FOR UPDATE, so do not override that using clear(). 01665 $this->loadPageData( 'fromdbmaster' ); 01666 01667 $flags = $this->checkFlags( $flags ); 01668 01669 // handle hook 01670 $hook_args = array( &$this, &$user, &$content, &$summary, 01671 $flags & EDIT_MINOR, null, null, &$flags, &$status ); 01672 01673 if ( !wfRunHooks( 'PageContentSave', $hook_args ) 01674 || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { 01675 01676 wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); 01677 01678 if ( $status->isOK() ) { 01679 $status->fatal( 'edit-hook-aborted' ); 01680 } 01681 01682 wfProfileOut( __METHOD__ ); 01683 return $status; 01684 } 01685 01686 // Silently ignore EDIT_MINOR if not allowed 01687 $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); 01688 $bot = $flags & EDIT_FORCE_BOT; 01689 01690 $old_content = $this->getContent( Revision::RAW ); // current revision's content 01691 01692 $oldsize = $old_content ? $old_content->getSize() : 0; 01693 $oldid = $this->getLatest(); 01694 $oldIsRedirect = $this->isRedirect(); 01695 $oldcountable = $this->isCountable(); 01696 01697 $handler = $content->getContentHandler(); 01698 01699 // Provide autosummaries if one is not provided and autosummaries are enabled. 01700 if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { 01701 if ( !$old_content ) $old_content = null; 01702 $summary = $handler->getAutosummary( $old_content, $content, $flags ); 01703 } 01704 01705 $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); 01706 $serialized = $editInfo->pst; 01707 $content = $editInfo->pstContent; 01708 $newsize = $content->getSize(); 01709 01710 $dbw = wfGetDB( DB_MASTER ); 01711 $now = wfTimestampNow(); 01712 $this->mTimestamp = $now; 01713 01714 if ( $flags & EDIT_UPDATE ) { 01715 // Update article, but only if changed. 01716 $status->value['new'] = false; 01717 01718 if ( !$oldid ) { 01719 // Article gone missing 01720 wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); 01721 $status->fatal( 'edit-gone-missing' ); 01722 01723 wfProfileOut( __METHOD__ ); 01724 return $status; 01725 } elseif ( !$old_content ) { 01726 // Sanity check for bug 37225 01727 wfProfileOut( __METHOD__ ); 01728 throw new MWException( "Could not find text for current revision {$oldid}." ); 01729 } 01730 01731 $revision = new Revision( array( 01732 'page' => $this->getId(), 01733 'title' => $this->getTitle(), // for determining the default content model 01734 'comment' => $summary, 01735 'minor_edit' => $isminor, 01736 'text' => $serialized, 01737 'len' => $newsize, 01738 'parent_id' => $oldid, 01739 'user' => $user->getId(), 01740 'user_text' => $user->getName(), 01741 'timestamp' => $now, 01742 'content_model' => $content->getModel(), 01743 'content_format' => $serialisation_format, 01744 ) ); // XXX: pass content object?! 01745 01746 $changed = !$content->equals( $old_content ); 01747 01748 if ( $changed ) { 01749 if ( !$content->isValid() ) { 01750 throw new MWException( "New content failed validity check!" ); 01751 } 01752 01753 $dbw->begin( __METHOD__ ); 01754 01755 $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); 01756 $status->merge( $prepStatus ); 01757 01758 if ( !$status->isOK() ) { 01759 $dbw->rollback( __METHOD__ ); 01760 01761 wfProfileOut( __METHOD__ ); 01762 return $status; 01763 } 01764 01765 $revisionId = $revision->insertOn( $dbw ); 01766 01767 // Update page 01768 // 01769 // Note that we use $this->mLatest instead of fetching a value from the master DB 01770 // during the course of this function. This makes sure that EditPage can detect 01771 // edit conflicts reliably, either by $ok here, or by $article->getTimestamp() 01772 // before this function is called. A previous function used a separate query, this 01773 // creates a window where concurrent edits can cause an ignored edit conflict. 01774 $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); 01775 01776 if ( !$ok ) { 01777 // Belated edit conflict! Run away!! 01778 $status->fatal( 'edit-conflict' ); 01779 01780 $dbw->rollback( __METHOD__ ); 01781 01782 wfProfileOut( __METHOD__ ); 01783 return $status; 01784 } 01785 01786 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); 01787 // Update recentchanges 01788 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01789 // Mark as patrolled if the user can do so 01790 $patrolled = $wgUseRCPatrol && !count( 01791 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01792 // Add RC row to the DB 01793 $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, 01794 $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, 01795 $revisionId, $patrolled 01796 ); 01797 01798 // Log auto-patrolled edits 01799 if ( $patrolled ) { 01800 PatrolLog::record( $rc, true, $user ); 01801 } 01802 } 01803 $user->incEditCount(); 01804 $dbw->commit( __METHOD__ ); 01805 } else { 01806 // Bug 32948: revision ID must be set to page {{REVISIONID}} and 01807 // related variables correctly 01808 $revision->setId( $this->getLatest() ); 01809 } 01810 01811 // Update links tables, site stats, etc. 01812 $this->doEditUpdates( 01813 $revision, 01814 $user, 01815 array( 01816 'changed' => $changed, 01817 'oldcountable' => $oldcountable 01818 ) 01819 ); 01820 01821 if ( !$changed ) { 01822 $status->warning( 'edit-no-change' ); 01823 $revision = null; 01824 // Update page_touched, this is usually implicit in the page update 01825 // Other cache updates are done in onArticleEdit() 01826 $this->mTitle->invalidateCache(); 01827 } 01828 } else { 01829 // Create new article 01830 $status->value['new'] = true; 01831 01832 $dbw->begin( __METHOD__ ); 01833 01834 $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); 01835 $status->merge( $prepStatus ); 01836 01837 if ( !$status->isOK() ) { 01838 $dbw->rollback( __METHOD__ ); 01839 01840 wfProfileOut( __METHOD__ ); 01841 return $status; 01842 } 01843 01844 $status->merge( $prepStatus ); 01845 01846 // Add the page record; stake our claim on this title! 01847 // This will return false if the article already exists 01848 $newid = $this->insertOn( $dbw ); 01849 01850 if ( $newid === false ) { 01851 $dbw->rollback( __METHOD__ ); 01852 $status->fatal( 'edit-already-exists' ); 01853 01854 wfProfileOut( __METHOD__ ); 01855 return $status; 01856 } 01857 01858 // Save the revision text... 01859 $revision = new Revision( array( 01860 'page' => $newid, 01861 'title' => $this->getTitle(), // for determining the default content model 01862 'comment' => $summary, 01863 'minor_edit' => $isminor, 01864 'text' => $serialized, 01865 'len' => $newsize, 01866 'user' => $user->getId(), 01867 'user_text' => $user->getName(), 01868 'timestamp' => $now, 01869 'content_model' => $content->getModel(), 01870 'content_format' => $serialisation_format, 01871 ) ); 01872 $revisionId = $revision->insertOn( $dbw ); 01873 01874 // Bug 37225: use accessor to get the text as Revision may trim it 01875 $content = $revision->getContent(); // sanity; get normalized version 01876 01877 if ( $content ) { 01878 $newsize = $content->getSize(); 01879 } 01880 01881 // Update the page record with revision data 01882 $this->updateRevisionOn( $dbw, $revision, 0 ); 01883 01884 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 01885 01886 // Update recentchanges 01887 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01888 // Mark as patrolled if the user can do so 01889 $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( 01890 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01891 // Add RC row to the DB 01892 $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, 01893 '', $newsize, $revisionId, $patrolled ); 01894 01895 // Log auto-patrolled edits 01896 if ( $patrolled ) { 01897 PatrolLog::record( $rc, true, $user ); 01898 } 01899 } 01900 $user->incEditCount(); 01901 $dbw->commit( __METHOD__ ); 01902 01903 // Update links, etc. 01904 $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); 01905 01906 $hook_args = array( &$this, &$user, $content, $summary, 01907 $flags & EDIT_MINOR, null, null, &$flags, $revision ); 01908 01909 ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); 01910 wfRunHooks( 'PageContentInsertComplete', $hook_args ); 01911 } 01912 01913 // Do updates right now unless deferral was requested 01914 if ( !( $flags & EDIT_DEFER_UPDATES ) ) { 01915 DeferredUpdates::doUpdates(); 01916 } 01917 01918 // Return the new revision (or null) to the caller 01919 $status->value['revision'] = $revision; 01920 01921 $hook_args = array( &$this, &$user, $content, $summary, 01922 $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); 01923 01924 ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); 01925 wfRunHooks( 'PageContentSaveComplete', $hook_args ); 01926 01927 // Promote user to any groups they meet the criteria for 01928 $user->addAutopromoteOnceGroups( 'onEdit' ); 01929 01930 wfProfileOut( __METHOD__ ); 01931 return $status; 01932 } 01933 01948 public function makeParserOptions( $context ) { 01949 $options = $this->getContentHandler()->makeParserOptions( $context ); 01950 01951 if ( $this->getTitle()->isConversionTable() ) { 01952 //@todo: ConversionTable should become a separate content model, so we don't need special cases like this one. 01953 $options->disableContentConversion(); 01954 } 01955 01956 return $options; 01957 } 01958 01965 public function prepareTextForEdit( $text, $revid = null, User $user = null ) { 01966 ContentHandler::deprecated( __METHOD__, '1.21' ); 01967 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 01968 return $this->prepareContentForEdit( $content, $revid, $user ); 01969 } 01970 01984 public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) { 01985 global $wgContLang, $wgUser; 01986 $user = is_null( $user ) ? $wgUser : $user; 01987 //XXX: check $user->getId() here??? 01988 01989 if ( $this->mPreparedEdit 01990 && $this->mPreparedEdit->newContent 01991 && $this->mPreparedEdit->newContent->equals( $content ) 01992 && $this->mPreparedEdit->revid == $revid 01993 && $this->mPreparedEdit->format == $serialization_format 01994 // XXX: also check $user here? 01995 ) { 01996 // Already prepared 01997 return $this->mPreparedEdit; 01998 } 01999 02000 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); 02001 wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) ); 02002 02003 $edit = (object)array(); 02004 $edit->revid = $revid; 02005 02006 $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null; 02007 02008 $edit->format = $serialization_format; 02009 $edit->popts = $this->makeParserOptions( 'canonical' ); 02010 $edit->output = $edit->pstContent ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) : null; 02011 02012 $edit->newContent = $content; 02013 $edit->oldContent = $this->getContent( Revision::RAW ); 02014 02015 // NOTE: B/C for hooks! don't use these fields! 02016 $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : ''; 02017 $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; 02018 $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : ''; 02019 02020 $this->mPreparedEdit = $edit; 02021 return $edit; 02022 } 02023 02040 public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { 02041 global $wgEnableParserCache; 02042 02043 wfProfileIn( __METHOD__ ); 02044 02045 $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); 02046 $content = $revision->getContent(); 02047 02048 // Parse the text 02049 // Be careful not to double-PST: $text is usually already PST-ed once 02050 if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { 02051 wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); 02052 $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); 02053 } else { 02054 wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); 02055 $editInfo = $this->mPreparedEdit; 02056 } 02057 02058 // Save it to the parser cache 02059 if ( $wgEnableParserCache ) { 02060 $parserCache = ParserCache::singleton(); 02061 $parserCache->save( $editInfo->output, $this, $editInfo->popts ); 02062 } 02063 02064 // Update the links tables and other secondary data 02065 if ( $content ) { 02066 $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output ); 02067 DataUpdate::runUpdates( $updates ); 02068 } 02069 02070 wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); 02071 02072 if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { 02073 if ( 0 == mt_rand( 0, 99 ) ) { 02074 // Flush old entries from the `recentchanges` table; we do this on 02075 // random requests so as to avoid an increase in writes for no good reason 02076 global $wgRCMaxAge; 02077 02078 $dbw = wfGetDB( DB_MASTER ); 02079 $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); 02080 $dbw->delete( 02081 'recentchanges', 02082 array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ), 02083 __METHOD__ 02084 ); 02085 } 02086 } 02087 02088 if ( !$this->exists() ) { 02089 wfProfileOut( __METHOD__ ); 02090 return; 02091 } 02092 02093 $id = $this->getId(); 02094 $title = $this->mTitle->getPrefixedDBkey(); 02095 $shortTitle = $this->mTitle->getDBkey(); 02096 02097 if ( !$options['changed'] ) { 02098 $good = 0; 02099 $total = 0; 02100 } elseif ( $options['created'] ) { 02101 $good = (int)$this->isCountable( $editInfo ); 02102 $total = 1; 02103 } elseif ( $options['oldcountable'] !== null ) { 02104 $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; 02105 $total = 0; 02106 } else { 02107 $good = 0; 02108 $total = 0; 02109 } 02110 02111 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); 02112 DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) ); 02113 // @TODO: let the search engine decide what to do with the content object 02114 02115 // If this is another user's talk page, update newtalk. 02116 // Don't do this if $options['changed'] = false (null-edits) nor if 02117 // it's a minor edit and the user doesn't want notifications for those. 02118 if ( $options['changed'] 02119 && $this->mTitle->getNamespace() == NS_USER_TALK 02120 && $shortTitle != $user->getTitleKey() 02121 && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) 02122 ) { 02123 if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) { 02124 $other = User::newFromName( $shortTitle, false ); 02125 if ( !$other ) { 02126 wfDebug( __METHOD__ . ": invalid username\n" ); 02127 } elseif ( User::isIP( $shortTitle ) ) { 02128 // An anonymous user 02129 $other->setNewtalk( true, $revision ); 02130 } elseif ( $other->isLoggedIn() ) { 02131 $other->setNewtalk( true, $revision ); 02132 } else { 02133 wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); 02134 } 02135 } 02136 } 02137 02138 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 02139 // XXX: could skip pseudo-messages like js/css here, based on content model. 02140 $msgtext = $content ? $content->getWikitextForTransclusion() : null; 02141 if ( $msgtext === false || $msgtext === null ) $msgtext = ''; 02142 02143 MessageCache::singleton()->replace( $shortTitle, $msgtext ); 02144 } 02145 02146 if( $options['created'] ) { 02147 self::onArticleCreate( $this->mTitle ); 02148 } else { 02149 self::onArticleEdit( $this->mTitle ); 02150 } 02151 02152 wfProfileOut( __METHOD__ ); 02153 } 02154 02167 public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { 02168 ContentHandler::deprecated( __METHOD__, "1.21" ); 02169 02170 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 02171 return $this->doQuickEditContent( $content, $user, $comment, $minor ); 02172 } 02173 02185 public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) { 02186 wfProfileIn( __METHOD__ ); 02187 02188 $serialized = $content->serialize( $serialisation_format ); 02189 02190 $dbw = wfGetDB( DB_MASTER ); 02191 $revision = new Revision( array( 02192 'title' => $this->getTitle(), // for determining the default content model 02193 'page' => $this->getId(), 02194 'text' => $serialized, 02195 'length' => $content->getSize(), 02196 'comment' => $comment, 02197 'minor_edit' => $minor ? 1 : 0, 02198 ) ); // XXX: set the content object? 02199 $revision->insertOn( $dbw ); 02200 $this->updateRevisionOn( $dbw, $revision ); 02201 02202 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 02203 02204 wfProfileOut( __METHOD__ ); 02205 } 02206 02218 public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) { 02219 global $wgContLang; 02220 02221 if ( wfReadOnly() ) { 02222 return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); 02223 } 02224 02225 $restrictionTypes = $this->mTitle->getRestrictionTypes(); 02226 02227 $id = $this->getId(); 02228 02229 if ( !$cascade ) { 02230 $cascade = false; 02231 } 02232 02233 // Take this opportunity to purge out expired restrictions 02234 Title::purgeExpiredRestrictions(); 02235 02236 // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); 02237 // we expect a single selection, but the schema allows otherwise. 02238 $isProtected = false; 02239 $protect = false; 02240 $changed = false; 02241 02242 $dbw = wfGetDB( DB_MASTER ); 02243 02244 foreach ( $restrictionTypes as $action ) { 02245 if ( !isset( $expiry[$action] ) ) { 02246 $expiry[$action] = $dbw->getInfinity(); 02247 } 02248 if ( !isset( $limit[$action] ) ) { 02249 $limit[$action] = ''; 02250 } elseif ( $limit[$action] != '' ) { 02251 $protect = true; 02252 } 02253 02254 // Get current restrictions on $action 02255 $current = implode( '', $this->mTitle->getRestrictions( $action ) ); 02256 if ( $current != '' ) { 02257 $isProtected = true; 02258 } 02259 02260 if ( $limit[$action] != $current ) { 02261 $changed = true; 02262 } elseif ( $limit[$action] != '' ) { 02263 // Only check expiry change if the action is actually being 02264 // protected, since expiry does nothing on an not-protected 02265 // action. 02266 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { 02267 $changed = true; 02268 } 02269 } 02270 } 02271 02272 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) { 02273 $changed = true; 02274 } 02275 02276 // If nothing has changed, do nothing 02277 if ( !$changed ) { 02278 return Status::newGood(); 02279 } 02280 02281 if ( !$protect ) { // No protection at all means unprotection 02282 $revCommentMsg = 'unprotectedarticle'; 02283 $logAction = 'unprotect'; 02284 } elseif ( $isProtected ) { 02285 $revCommentMsg = 'modifiedarticleprotection'; 02286 $logAction = 'modify'; 02287 } else { 02288 $revCommentMsg = 'protectedarticle'; 02289 $logAction = 'protect'; 02290 } 02291 02292 $encodedExpiry = array(); 02293 $protectDescription = ''; 02294 # Some bots may parse IRC lines, which are generated from log entries which contain plain 02295 # protect description text. Keep them in old format to avoid breaking compatibility. 02296 # TODO: Fix protection log to store structured description and format it on-the-fly. 02297 $protectDescriptionLog = ''; 02298 foreach ( $limit as $action => $restrictions ) { 02299 $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] ); 02300 if ( $restrictions != '' ) { 02301 $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ("; 02302 # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ). 02303 # All possible message keys are listed here for easier grepping: 02304 # * restriction-create 02305 # * restriction-edit 02306 # * restriction-move 02307 # * restriction-upload 02308 $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text(); 02309 # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ), 02310 # with '' filtered out. All possible message keys are listed below: 02311 # * protect-level-autoconfirmed 02312 # * protect-level-sysop 02313 $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text(); 02314 if ( $encodedExpiry[$action] != 'infinity' ) { 02315 $expiryText = wfMessage( 02316 'protect-expiring', 02317 $wgContLang->timeanddate( $expiry[$action], false, false ), 02318 $wgContLang->date( $expiry[$action], false, false ), 02319 $wgContLang->time( $expiry[$action], false, false ) 02320 )->inContentLanguage()->text(); 02321 } else { 02322 $expiryText = wfMessage( 'protect-expiry-indefinite' ) 02323 ->inContentLanguage()->text(); 02324 } 02325 02326 if ( $protectDescription !== '' ) { 02327 $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02328 } 02329 $protectDescription .= wfMessage( 'protect-summary-desc' ) 02330 ->params( $actionText, $restrictionsText, $expiryText ) 02331 ->inContentLanguage()->text(); 02332 $protectDescriptionLog .= $expiryText . ') '; 02333 } 02334 } 02335 $protectDescriptionLog = trim( $protectDescriptionLog ); 02336 02337 if ( $id ) { // Protection of existing page 02338 if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { 02339 return Status::newGood(); 02340 } 02341 02342 // Only restrictions with the 'protect' right can cascade... 02343 // Otherwise, people who cannot normally protect can "protect" pages via transclusion 02344 $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); 02345 02346 // The schema allows multiple restrictions 02347 if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) { 02348 $cascade = false; 02349 } 02350 02351 // Update restrictions table 02352 foreach ( $limit as $action => $restrictions ) { 02353 if ( $restrictions != '' ) { 02354 $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ), 02355 array( 'pr_page' => $id, 02356 'pr_type' => $action, 02357 'pr_level' => $restrictions, 02358 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, 02359 'pr_expiry' => $encodedExpiry[$action] 02360 ), 02361 __METHOD__ 02362 ); 02363 } else { 02364 $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 02365 'pr_type' => $action ), __METHOD__ ); 02366 } 02367 } 02368 02369 // Prepare a null revision to be added to the history 02370 $editComment = $wgContLang->ucfirst( 02371 wfMessage( 02372 $revCommentMsg, 02373 $this->mTitle->getPrefixedText() 02374 )->inContentLanguage()->text() 02375 ); 02376 if ( $reason ) { 02377 $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 02378 } 02379 if ( $protectDescription ) { 02380 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02381 $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )->inContentLanguage()->text(); 02382 } 02383 if ( $cascade ) { 02384 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02385 $editComment .= wfMessage( 'brackets' )->params( 02386 wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text() 02387 )->inContentLanguage()->text(); 02388 } 02389 02390 // Insert a null revision 02391 $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true ); 02392 $nullRevId = $nullRevision->insertOn( $dbw ); 02393 02394 $latest = $this->getLatest(); 02395 // Update page record 02396 $dbw->update( 'page', 02397 array( /* SET */ 02398 'page_touched' => $dbw->timestamp(), 02399 'page_restrictions' => '', 02400 'page_latest' => $nullRevId 02401 ), array( /* WHERE */ 02402 'page_id' => $id 02403 ), __METHOD__ 02404 ); 02405 02406 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); 02407 wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); 02408 } else { // Protection of non-existing page (also known as "title protection") 02409 // Cascade protection is meaningless in this case 02410 $cascade = false; 02411 02412 if ( $limit['create'] != '' ) { 02413 $dbw->replace( 'protected_titles', 02414 array( array( 'pt_namespace', 'pt_title' ) ), 02415 array( 02416 'pt_namespace' => $this->mTitle->getNamespace(), 02417 'pt_title' => $this->mTitle->getDBkey(), 02418 'pt_create_perm' => $limit['create'], 02419 'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ), 02420 'pt_expiry' => $encodedExpiry['create'], 02421 'pt_user' => $user->getId(), 02422 'pt_reason' => $reason, 02423 ), __METHOD__ 02424 ); 02425 } else { 02426 $dbw->delete( 'protected_titles', 02427 array( 02428 'pt_namespace' => $this->mTitle->getNamespace(), 02429 'pt_title' => $this->mTitle->getDBkey() 02430 ), __METHOD__ 02431 ); 02432 } 02433 } 02434 02435 $this->mTitle->flushRestrictions(); 02436 02437 if ( $logAction == 'unprotect' ) { 02438 $logParams = array(); 02439 } else { 02440 $logParams = array( $protectDescriptionLog, $cascade ? 'cascade' : '' ); 02441 } 02442 02443 // Update the protection log 02444 $log = new LogPage( 'protect' ); 02445 $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user ); 02446 02447 return Status::newGood(); 02448 } 02449 02457 protected static function flattenRestrictions( $limit ) { 02458 if ( !is_array( $limit ) ) { 02459 throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' ); 02460 } 02461 02462 $bits = array(); 02463 ksort( $limit ); 02464 02465 foreach ( $limit as $action => $restrictions ) { 02466 if ( $restrictions != '' ) { 02467 $bits[] = "$action=$restrictions"; 02468 } 02469 } 02470 02471 return implode( ':', $bits ); 02472 } 02473 02490 public function doDeleteArticle( 02491 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02492 ) { 02493 $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ); 02494 return $status->isGood(); 02495 } 02496 02514 public function doDeleteArticleReal( 02515 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02516 ) { 02517 global $wgUser, $wgContentHandlerUseDB; 02518 02519 wfDebug( __METHOD__ . "\n" ); 02520 02521 $status = Status::newGood(); 02522 02523 if ( $this->mTitle->getDBkey() === '' ) { 02524 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02525 return $status; 02526 } 02527 02528 $user = is_null( $user ) ? $wgUser : $user; 02529 if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) { 02530 if ( $status->isOK() ) { 02531 // Hook aborted but didn't set a fatal status 02532 $status->fatal( 'delete-hook-aborted' ); 02533 } 02534 return $status; 02535 } 02536 02537 if ( $id == 0 ) { 02538 $this->loadPageData( 'forupdate' ); 02539 $id = $this->getID(); 02540 if ( $id == 0 ) { 02541 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02542 return $status; 02543 } 02544 } 02545 02546 // Bitfields to further suppress the content 02547 if ( $suppress ) { 02548 $bitfield = 0; 02549 // This should be 15... 02550 $bitfield |= Revision::DELETED_TEXT; 02551 $bitfield |= Revision::DELETED_COMMENT; 02552 $bitfield |= Revision::DELETED_USER; 02553 $bitfield |= Revision::DELETED_RESTRICTED; 02554 } else { 02555 $bitfield = 'rev_deleted'; 02556 } 02557 02558 // we need to remember the old content so we can use it to generate all deletion updates. 02559 $content = $this->getContent( Revision::RAW ); 02560 02561 $dbw = wfGetDB( DB_MASTER ); 02562 $dbw->begin( __METHOD__ ); 02563 // For now, shunt the revision data into the archive table. 02564 // Text is *not* removed from the text table; bulk storage 02565 // is left intact to avoid breaking block-compression or 02566 // immutable storage schemes. 02567 // 02568 // For backwards compatibility, note that some older archive 02569 // table entries will have ar_text and ar_flags fields still. 02570 // 02571 // In the future, we may keep revisions and mark them with 02572 // the rev_deleted field, which is reserved for this purpose. 02573 02574 $row = array( 02575 'ar_namespace' => 'page_namespace', 02576 'ar_title' => 'page_title', 02577 'ar_comment' => 'rev_comment', 02578 'ar_user' => 'rev_user', 02579 'ar_user_text' => 'rev_user_text', 02580 'ar_timestamp' => 'rev_timestamp', 02581 'ar_minor_edit' => 'rev_minor_edit', 02582 'ar_rev_id' => 'rev_id', 02583 'ar_parent_id' => 'rev_parent_id', 02584 'ar_text_id' => 'rev_text_id', 02585 'ar_text' => '\'\'', // Be explicit to appease 02586 'ar_flags' => '\'\'', // MySQL's "strict mode"... 02587 'ar_len' => 'rev_len', 02588 'ar_page_id' => 'page_id', 02589 'ar_deleted' => $bitfield, 02590 'ar_sha1' => 'rev_sha1', 02591 ); 02592 02593 if ( $wgContentHandlerUseDB ) { 02594 $row[ 'ar_content_model' ] = 'rev_content_model'; 02595 $row[ 'ar_content_format' ] = 'rev_content_format'; 02596 } 02597 02598 $dbw->insertSelect( 'archive', array( 'page', 'revision' ), 02599 $row, 02600 array( 02601 'page_id' => $id, 02602 'page_id = rev_page' 02603 ), __METHOD__ 02604 ); 02605 02606 // Now that it's safely backed up, delete it 02607 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); 02608 $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy 02609 02610 if ( !$ok ) { 02611 $dbw->rollback( __METHOD__ ); 02612 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02613 return $status; 02614 } 02615 02616 $this->doDeleteUpdates( $id, $content ); 02617 02618 // Log the deletion, if the page was suppressed, log it at Oversight instead 02619 $logtype = $suppress ? 'suppress' : 'delete'; 02620 02621 $logEntry = new ManualLogEntry( $logtype, 'delete' ); 02622 $logEntry->setPerformer( $user ); 02623 $logEntry->setTarget( $this->mTitle ); 02624 $logEntry->setComment( $reason ); 02625 $logid = $logEntry->insert(); 02626 $logEntry->publish( $logid ); 02627 02628 if ( $commit ) { 02629 $dbw->commit( __METHOD__ ); 02630 } 02631 02632 wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); 02633 $status->value = $logid; 02634 return $status; 02635 } 02636 02644 public function doDeleteUpdates( $id, Content $content = null ) { 02645 // update site status 02646 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); 02647 02648 // remove secondary indexes, etc 02649 $updates = $this->getDeletionUpdates( $content ); 02650 DataUpdate::runUpdates( $updates ); 02651 02652 // Clear caches 02653 WikiPage::onArticleDelete( $this->mTitle ); 02654 02655 // Reset this object and the Title object 02656 $this->loadFromRow( false, self::READ_LATEST ); 02657 } 02658 02683 public function doRollback( 02684 $fromP, $summary, $token, $bot, &$resultDetails, User $user 02685 ) { 02686 $resultDetails = null; 02687 02688 // Check permissions 02689 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); 02690 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); 02691 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); 02692 02693 if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { 02694 $errors[] = array( 'sessionfailure' ); 02695 } 02696 02697 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) { 02698 $errors[] = array( 'actionthrottledtext' ); 02699 } 02700 02701 // If there were errors, bail out now 02702 if ( !empty( $errors ) ) { 02703 return $errors; 02704 } 02705 02706 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user ); 02707 } 02708 02725 public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { 02726 global $wgUseRCPatrol, $wgContLang; 02727 02728 $dbw = wfGetDB( DB_MASTER ); 02729 02730 if ( wfReadOnly() ) { 02731 return array( array( 'readonlytext' ) ); 02732 } 02733 02734 // Get the last editor 02735 $current = $this->getRevision(); 02736 if ( is_null( $current ) ) { 02737 // Something wrong... no page? 02738 return array( array( 'notanarticle' ) ); 02739 } 02740 02741 $from = str_replace( '_', ' ', $fromP ); 02742 // User name given should match up with the top revision. 02743 // If the user was deleted then $from should be empty. 02744 if ( $from != $current->getUserText() ) { 02745 $resultDetails = array( 'current' => $current ); 02746 return array( array( 'alreadyrolled', 02747 htmlspecialchars( $this->mTitle->getPrefixedText() ), 02748 htmlspecialchars( $fromP ), 02749 htmlspecialchars( $current->getUserText() ) 02750 ) ); 02751 } 02752 02753 // Get the last edit not by this guy... 02754 // Note: these may not be public values 02755 $user = intval( $current->getRawUser() ); 02756 $user_text = $dbw->addQuotes( $current->getRawUserText() ); 02757 $s = $dbw->selectRow( 'revision', 02758 array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), 02759 array( 'rev_page' => $current->getPage(), 02760 "rev_user != {$user} OR rev_user_text != {$user_text}" 02761 ), __METHOD__, 02762 array( 'USE INDEX' => 'page_timestamp', 02763 'ORDER BY' => 'rev_timestamp DESC' ) 02764 ); 02765 if ( $s === false ) { 02766 // No one else ever edited this page 02767 return array( array( 'cantrollback' ) ); 02768 } elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) { 02769 // Only admins can see this text 02770 return array( array( 'notvisiblerev' ) ); 02771 } 02772 02773 $set = array(); 02774 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { 02775 // Mark all reverted edits as bot 02776 $set['rc_bot'] = 1; 02777 } 02778 02779 if ( $wgUseRCPatrol ) { 02780 // Mark all reverted edits as patrolled 02781 $set['rc_patrolled'] = 1; 02782 } 02783 02784 if ( count( $set ) ) { 02785 $dbw->update( 'recentchanges', $set, 02786 array( /* WHERE */ 02787 'rc_cur_id' => $current->getPage(), 02788 'rc_user_text' => $current->getUserText(), 02789 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), 02790 ), __METHOD__ 02791 ); 02792 } 02793 02794 // Generate the edit summary if necessary 02795 $target = Revision::newFromId( $s->rev_id ); 02796 if ( empty( $summary ) ) { 02797 if ( $from == '' ) { // no public user name 02798 $summary = wfMessage( 'revertpage-nouser' ); 02799 } else { 02800 $summary = wfMessage( 'revertpage' ); 02801 } 02802 } 02803 02804 // Allow the custom summary to use the same args as the default message 02805 $args = array( 02806 $target->getUserText(), $from, $s->rev_id, 02807 $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), 02808 $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) 02809 ); 02810 if( $summary instanceof Message ) { 02811 $summary = $summary->params( $args )->inContentLanguage()->text(); 02812 } else { 02813 $summary = wfMsgReplaceArgs( $summary, $args ); 02814 } 02815 02816 // Trim spaces on user supplied text 02817 $summary = trim( $summary ); 02818 02819 // Truncate for whole multibyte characters. 02820 $summary = $wgContLang->truncate( $summary, 255 ); 02821 02822 // Save 02823 $flags = EDIT_UPDATE; 02824 02825 if ( $guser->isAllowed( 'minoredit' ) ) { 02826 $flags |= EDIT_MINOR; 02827 } 02828 02829 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { 02830 $flags |= EDIT_FORCE_BOT; 02831 } 02832 02833 // Actually store the edit 02834 $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser ); 02835 02836 if ( !$status->isOK() ) { 02837 return $status->getErrorsArray(); 02838 } 02839 02840 if ( !empty( $status->value['revision'] ) ) { 02841 $revId = $status->value['revision']->getId(); 02842 } else { 02843 $revId = false; 02844 } 02845 02846 wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) ); 02847 02848 $resultDetails = array( 02849 'summary' => $summary, 02850 'current' => $current, 02851 'target' => $target, 02852 'newid' => $revId 02853 ); 02854 02855 return array(); 02856 } 02857 02869 public static function onArticleCreate( $title ) { 02870 // Update existence markers on article/talk tabs... 02871 if ( $title->isTalkPage() ) { 02872 $other = $title->getSubjectPage(); 02873 } else { 02874 $other = $title->getTalkPage(); 02875 } 02876 02877 $other->invalidateCache(); 02878 $other->purgeSquid(); 02879 02880 $title->touchLinks(); 02881 $title->purgeSquid(); 02882 $title->deleteTitleProtection(); 02883 } 02884 02890 public static function onArticleDelete( $title ) { 02891 // Update existence markers on article/talk tabs... 02892 if ( $title->isTalkPage() ) { 02893 $other = $title->getSubjectPage(); 02894 } else { 02895 $other = $title->getTalkPage(); 02896 } 02897 02898 $other->invalidateCache(); 02899 $other->purgeSquid(); 02900 02901 $title->touchLinks(); 02902 $title->purgeSquid(); 02903 02904 // File cache 02905 HTMLFileCache::clearFileCache( $title ); 02906 02907 // Messages 02908 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 02909 MessageCache::singleton()->replace( $title->getDBkey(), false ); 02910 } 02911 02912 // Images 02913 if ( $title->getNamespace() == NS_FILE ) { 02914 $update = new HTMLCacheUpdate( $title, 'imagelinks' ); 02915 $update->doUpdate(); 02916 } 02917 02918 // User talk pages 02919 if ( $title->getNamespace() == NS_USER_TALK ) { 02920 $user = User::newFromName( $title->getText(), false ); 02921 if ( $user ) { 02922 $user->setNewtalk( false ); 02923 } 02924 } 02925 02926 // Image redirects 02927 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); 02928 } 02929 02936 public static function onArticleEdit( $title ) { 02937 // Invalidate caches of articles which include this page 02938 DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); 02939 02940 // Invalidate the caches of all pages which redirect here 02941 DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); 02942 02943 // Purge squid for this page only 02944 $title->purgeSquid(); 02945 02946 // Clear file cache for this page only 02947 HTMLFileCache::clearFileCache( $title ); 02948 } 02949 02958 public function getHiddenCategories() { 02959 $result = array(); 02960 $id = $this->getId(); 02961 02962 if ( $id == 0 ) { 02963 return array(); 02964 } 02965 02966 $dbr = wfGetDB( DB_SLAVE ); 02967 $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), 02968 array( 'cl_to' ), 02969 array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', 02970 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), 02971 __METHOD__ ); 02972 02973 if ( $res !== false ) { 02974 foreach ( $res as $row ) { 02975 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); 02976 } 02977 } 02978 02979 return $result; 02980 } 02981 02991 public static function getAutosummary( $oldtext, $newtext, $flags ) { 02992 // NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't. 02993 02994 ContentHandler::deprecated( __METHOD__, '1.21' ); 02995 02996 $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); 02997 $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext ); 02998 $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext ); 02999 03000 return $handler->getAutosummary( $oldContent, $newContent, $flags ); 03001 } 03002 03010 public function getAutoDeleteReason( &$hasHistory ) { 03011 return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory ); 03012 } 03013 03021 public function updateCategoryCounts( $added, $deleted ) { 03022 $ns = $this->mTitle->getNamespace(); 03023 $dbw = wfGetDB( DB_MASTER ); 03024 03025 // First make sure the rows exist. If one of the "deleted" ones didn't 03026 // exist, we might legitimately not create it, but it's simpler to just 03027 // create it and then give it a negative value, since the value is bogus 03028 // anyway. 03029 // 03030 // Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. 03031 $insertCats = array_merge( $added, $deleted ); 03032 if ( !$insertCats ) { 03033 // Okay, nothing to do 03034 return; 03035 } 03036 03037 $insertRows = array(); 03038 03039 foreach ( $insertCats as $cat ) { 03040 $insertRows[] = array( 03041 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ), 03042 'cat_title' => $cat 03043 ); 03044 } 03045 $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); 03046 03047 $addFields = array( 'cat_pages = cat_pages + 1' ); 03048 $removeFields = array( 'cat_pages = cat_pages - 1' ); 03049 03050 if ( $ns == NS_CATEGORY ) { 03051 $addFields[] = 'cat_subcats = cat_subcats + 1'; 03052 $removeFields[] = 'cat_subcats = cat_subcats - 1'; 03053 } elseif ( $ns == NS_FILE ) { 03054 $addFields[] = 'cat_files = cat_files + 1'; 03055 $removeFields[] = 'cat_files = cat_files - 1'; 03056 } 03057 03058 if ( $added ) { 03059 $dbw->update( 03060 'category', 03061 $addFields, 03062 array( 'cat_title' => $added ), 03063 __METHOD__ 03064 ); 03065 } 03066 03067 if ( $deleted ) { 03068 $dbw->update( 03069 'category', 03070 $removeFields, 03071 array( 'cat_title' => $deleted ), 03072 __METHOD__ 03073 ); 03074 } 03075 03076 foreach( $added as $catName ) { 03077 $cat = Category::newFromName( $catName ); 03078 wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $this ) ); 03079 } 03080 foreach( $deleted as $catName ) { 03081 $cat = Category::newFromName( $catName ); 03082 wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $this ) ); 03083 } 03084 } 03085 03091 public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) { 03092 if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { 03093 return; 03094 } 03095 03096 // templatelinks table may have become out of sync, 03097 // especially if using variable-based transclusions. 03098 // For paranoia, check if things have changed and if 03099 // so apply updates to the database. This will ensure 03100 // that cascaded protections apply as soon as the changes 03101 // are visible. 03102 03103 // Get templates from templatelinks 03104 $id = $this->getId(); 03105 03106 $tlTemplates = array(); 03107 03108 $dbr = wfGetDB( DB_SLAVE ); 03109 $res = $dbr->select( array( 'templatelinks' ), 03110 array( 'tl_namespace', 'tl_title' ), 03111 array( 'tl_from' => $id ), 03112 __METHOD__ 03113 ); 03114 03115 foreach ( $res as $row ) { 03116 $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; 03117 } 03118 03119 // Get templates from parser output. 03120 $poTemplates = array(); 03121 foreach ( $parserOutput->getTemplates() as $ns => $templates ) { 03122 foreach ( $templates as $dbk => $id ) { 03123 $poTemplates["$ns:$dbk"] = true; 03124 } 03125 } 03126 03127 // Get the diff 03128 $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); 03129 03130 if ( count( $templates_diff ) > 0 ) { 03131 // Whee, link updates time. 03132 // Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output. 03133 $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); 03134 $u->doUpdate(); 03135 } 03136 } 03137 03145 public function getUsedTemplates() { 03146 return $this->mTitle->getTemplateLinksFrom(); 03147 } 03148 03159 public function createUpdates( $rev ) { 03160 wfDeprecated( __METHOD__, '1.18' ); 03161 global $wgUser; 03162 $this->doEditUpdates( $rev, $wgUser, array( 'created' => true ) ); 03163 } 03164 03177 public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) { 03178 global $wgParser, $wgUser; 03179 03180 wfDeprecated( __METHOD__, '1.19' ); 03181 03182 $user = is_null( $user ) ? $wgUser : $user; 03183 03184 if ( $popts === null ) { 03185 $popts = ParserOptions::newFromUser( $user ); 03186 } 03187 03188 return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); 03189 } 03190 03197 public function isBigDeletion() { 03198 wfDeprecated( __METHOD__, '1.19' ); 03199 return $this->mTitle->isBigDeletion(); 03200 } 03201 03208 public function estimateRevisionCount() { 03209 wfDeprecated( __METHOD__, '1.19' ); 03210 return $this->mTitle->estimateRevisionCount(); 03211 } 03212 03224 public function updateRestrictions( 03225 $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null 03226 ) { 03227 global $wgUser; 03228 03229 $user = is_null( $user ) ? $wgUser : $user; 03230 03231 return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK(); 03232 } 03233 03237 public function quickEdit( $text, $comment = '', $minor = 0 ) { 03238 wfDeprecated( __METHOD__, '1.18' ); 03239 global $wgUser; 03240 $this->doQuickEdit( $text, $wgUser, $comment, $minor ); 03241 } 03242 03246 public function viewUpdates() { 03247 wfDeprecated( __METHOD__, '1.18' ); 03248 global $wgUser; 03249 return $this->doViewUpdates( $wgUser ); 03250 } 03251 03257 public function useParserCache( $oldid ) { 03258 wfDeprecated( __METHOD__, '1.18' ); 03259 global $wgUser; 03260 return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid ); 03261 } 03262 03270 public function getDeletionUpdates( Content $content = null ) { 03271 if ( !$content ) { 03272 // load content object, which may be used to determine the necessary updates 03273 // XXX: the content may not be needed to determine the updates, then this would be overhead. 03274 $content = $this->getContent( Revision::RAW ); 03275 } 03276 03277 if ( !$content ) { 03278 $updates = array(); 03279 } else { 03280 $updates = $content->getDeletionUpdates( $this ); 03281 } 03282 03283 wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); 03284 return $updates; 03285 } 03286 03287 } 03288 03289 class PoolWorkArticleView extends PoolCounterWork { 03290 03294 private $page; 03295 03299 private $cacheKey; 03300 03304 private $revid; 03305 03309 private $parserOptions; 03310 03314 private $content = null; 03315 03319 private $parserOutput = false; 03320 03324 private $isDirty = false; 03325 03329 private $error = false; 03330 03340 function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) { 03341 if ( is_string( $content ) ) { // BC: old style call 03342 $modelId = $page->getRevision()->getContentModel(); 03343 $format = $page->getRevision()->getContentFormat(); 03344 $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format ); 03345 } 03346 03347 $this->page = $page; 03348 $this->revid = $revid; 03349 $this->cacheable = $useParserCache; 03350 $this->parserOptions = $parserOptions; 03351 $this->content = $content; 03352 $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); 03353 parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); 03354 } 03355 03361 public function getParserOutput() { 03362 return $this->parserOutput; 03363 } 03364 03370 public function getIsDirty() { 03371 return $this->isDirty; 03372 } 03373 03379 public function getError() { 03380 return $this->error; 03381 } 03382 03386 function doWork() { 03387 global $wgUseFileCache; 03388 03389 // @todo: several of the methods called on $this->page are not declared in Page, but present 03390 // in WikiPage and delegated by Article. 03391 03392 $isCurrent = $this->revid === $this->page->getLatest(); 03393 03394 if ( $this->content !== null ) { 03395 $content = $this->content; 03396 } elseif ( $isCurrent ) { 03397 // XXX: why use RAW audience here, and PUBLIC (default) below? 03398 $content = $this->page->getContent( Revision::RAW ); 03399 } else { 03400 $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); 03401 03402 if ( $rev === null ) { 03403 $content = null; 03404 } else { 03405 // XXX: why use PUBLIC audience here (default), and RAW above? 03406 $content = $rev->getContent(); 03407 } 03408 } 03409 03410 if ( $content === null ) { 03411 return false; 03412 } 03413 03414 $time = - microtime( true ); 03415 $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions ); 03416 $time += microtime( true ); 03417 03418 // Timing hack 03419 if ( $time > 3 ) { 03420 wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, 03421 $this->page->getTitle()->getPrefixedDBkey() ) ); 03422 } 03423 03424 if ( $this->cacheable && $this->parserOutput->isCacheable() ) { 03425 ParserCache::singleton()->save( $this->parserOutput, $this->page, $this->parserOptions ); 03426 } 03427 03428 // Make sure file cache is not used on uncacheable content. 03429 // Output that has magic words in it can still use the parser cache 03430 // (if enabled), though it will generally expire sooner. 03431 if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) { 03432 $wgUseFileCache = false; 03433 } 03434 03435 if ( $isCurrent ) { 03436 $this->page->doCascadeProtectionUpdates( $this->parserOutput ); 03437 } 03438 03439 return true; 03440 } 03441 03445 function getCachedWork() { 03446 $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions ); 03447 03448 if ( $this->parserOutput === false ) { 03449 wfDebug( __METHOD__ . ": parser cache miss\n" ); 03450 return false; 03451 } else { 03452 wfDebug( __METHOD__ . ": parser cache hit\n" ); 03453 return true; 03454 } 03455 } 03456 03460 function fallback() { 03461 $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions ); 03462 03463 if ( $this->parserOutput === false ) { 03464 wfDebugLog( 'dirty', "dirty missing\n" ); 03465 wfDebug( __METHOD__ . ": no dirty cache\n" ); 03466 return false; 03467 } else { 03468 wfDebug( __METHOD__ . ": sending dirty output\n" ); 03469 wfDebugLog( 'dirty', "dirty output {$this->cacheKey}\n" ); 03470 $this->isDirty = true; 03471 return true; 03472 } 03473 } 03474 03479 function error( $status ) { 03480 $this->error = $status; 03481 return false; 03482 } 03483 }