MediaWiki
REL1_20
|
00001 <?php 00026 abstract class Page {} 00027 00036 class WikiPage extends Page implements 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 return array(); 00191 } 00192 00197 public function getTitle() { 00198 return $this->mTitle; 00199 } 00200 00205 public function clear() { 00206 $this->mDataLoaded = false; 00207 $this->mDataLoadedFrom = self::READ_NONE; 00208 00209 $this->clearCacheFields(); 00210 } 00211 00216 protected function clearCacheFields() { 00217 $this->mCounter = null; 00218 $this->mRedirectTarget = null; # Title object if set 00219 $this->mLastRevision = null; # Latest revision 00220 $this->mTouched = '19700101000000'; 00221 $this->mTimestamp = ''; 00222 $this->mIsRedirect = false; 00223 $this->mLatest = false; 00224 $this->mPreparedEdit = false; 00225 } 00226 00233 public static function selectFields() { 00234 return array( 00235 'page_id', 00236 'page_namespace', 00237 'page_title', 00238 'page_restrictions', 00239 'page_counter', 00240 'page_is_redirect', 00241 'page_is_new', 00242 'page_random', 00243 'page_touched', 00244 'page_latest', 00245 'page_len', 00246 ); 00247 } 00248 00256 protected function pageData( $dbr, $conditions, $options = array() ) { 00257 $fields = self::selectFields(); 00258 00259 wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); 00260 00261 $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); 00262 00263 wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); 00264 00265 return $row; 00266 } 00267 00277 public function pageDataFromTitle( $dbr, $title, $options = array() ) { 00278 return $this->pageData( $dbr, array( 00279 'page_namespace' => $title->getNamespace(), 00280 'page_title' => $title->getDBkey() ), $options ); 00281 } 00282 00291 public function pageDataFromId( $dbr, $id, $options = array() ) { 00292 return $this->pageData( $dbr, array( 'page_id' => $id ), $options ); 00293 } 00294 00307 public function loadPageData( $from = 'fromdb' ) { 00308 $from = self::convertSelectType( $from ); 00309 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) { 00310 // We already have the data from the correct location, no need to load it twice. 00311 return; 00312 } 00313 00314 if ( $from === self::READ_LOCKING ) { 00315 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); 00316 } elseif ( $from === self::READ_LATEST ) { 00317 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00318 } elseif ( $from === self::READ_NORMAL ) { 00319 $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); 00320 # Use a "last rev inserted" timestamp key to dimish the issue of slave lag. 00321 # Note that DB also stores the master position in the session and checks it. 00322 $touched = $this->getCachedLastEditTime(); 00323 if ( $touched ) { // key set 00324 if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { 00325 $from = self::READ_LATEST; 00326 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00327 } 00328 } 00329 } else { 00330 // No idea from where the caller got this data, assume slave database. 00331 $data = $from; 00332 $from = self::READ_NORMAL; 00333 } 00334 00335 $this->loadFromRow( $data, $from ); 00336 } 00337 00350 public function loadFromRow( $data, $from ) { 00351 $lc = LinkCache::singleton(); 00352 00353 if ( $data ) { 00354 $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); 00355 00356 $this->mTitle->loadFromRow( $data ); 00357 00358 # Old-fashioned restrictions 00359 $this->mTitle->loadRestrictions( $data->page_restrictions ); 00360 00361 $this->mCounter = intval( $data->page_counter ); 00362 $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); 00363 $this->mIsRedirect = intval( $data->page_is_redirect ); 00364 $this->mLatest = intval( $data->page_latest ); 00365 // Bug 37225: $latest may no longer match the cached latest Revision object. 00366 // Double-check the ID of any cached latest Revision object for consistency. 00367 if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { 00368 $this->mLastRevision = null; 00369 $this->mTimestamp = ''; 00370 } 00371 } else { 00372 $lc->addBadLinkObj( $this->mTitle ); 00373 00374 $this->mTitle->loadFromRow( false ); 00375 00376 $this->clearCacheFields(); 00377 } 00378 00379 $this->mDataLoaded = true; 00380 $this->mDataLoadedFrom = self::convertSelectType( $from ); 00381 } 00382 00386 public function getId() { 00387 return $this->mTitle->getArticleID(); 00388 } 00389 00393 public function exists() { 00394 return $this->mTitle->exists(); 00395 } 00396 00405 public function hasViewableContent() { 00406 return $this->mTitle->exists() || $this->mTitle->isAlwaysKnown(); 00407 } 00408 00412 public function getCount() { 00413 if ( !$this->mDataLoaded ) { 00414 $this->loadPageData(); 00415 } 00416 00417 return $this->mCounter; 00418 } 00419 00426 public function isRedirect( $text = false ) { 00427 if ( $text === false ) { 00428 if ( !$this->mDataLoaded ) { 00429 $this->loadPageData(); 00430 } 00431 00432 return (bool)$this->mIsRedirect; 00433 } else { 00434 return Title::newFromRedirect( $text ) !== null; 00435 } 00436 } 00437 00442 public function checkTouched() { 00443 if ( !$this->mDataLoaded ) { 00444 $this->loadPageData(); 00445 } 00446 return !$this->mIsRedirect; 00447 } 00448 00453 public function getTouched() { 00454 if ( !$this->mDataLoaded ) { 00455 $this->loadPageData(); 00456 } 00457 return $this->mTouched; 00458 } 00459 00464 public function getLatest() { 00465 if ( !$this->mDataLoaded ) { 00466 $this->loadPageData(); 00467 } 00468 return (int)$this->mLatest; 00469 } 00470 00475 public function getOldestRevision() { 00476 wfProfileIn( __METHOD__ ); 00477 00478 // Try using the slave database first, then try the master 00479 $continue = 2; 00480 $db = wfGetDB( DB_SLAVE ); 00481 $revSelectFields = Revision::selectFields(); 00482 00483 while ( $continue ) { 00484 $row = $db->selectRow( 00485 array( 'page', 'revision' ), 00486 $revSelectFields, 00487 array( 00488 'page_namespace' => $this->mTitle->getNamespace(), 00489 'page_title' => $this->mTitle->getDBkey(), 00490 'rev_page = page_id' 00491 ), 00492 __METHOD__, 00493 array( 00494 'ORDER BY' => 'rev_timestamp ASC' 00495 ) 00496 ); 00497 00498 if ( $row ) { 00499 $continue = 0; 00500 } else { 00501 $db = wfGetDB( DB_MASTER ); 00502 $continue--; 00503 } 00504 } 00505 00506 wfProfileOut( __METHOD__ ); 00507 return $row ? Revision::newFromRow( $row ) : null; 00508 } 00509 00514 protected function loadLastEdit() { 00515 if ( $this->mLastRevision !== null ) { 00516 return; // already loaded 00517 } 00518 00519 $latest = $this->getLatest(); 00520 if ( !$latest ) { 00521 return; // page doesn't exist or is missing page_latest info 00522 } 00523 00524 // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the 00525 // latest changes committed. This is true even within REPEATABLE-READ transactions, where 00526 // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to 00527 // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row 00528 // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. 00529 // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. 00530 $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; 00531 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); 00532 if ( $revision ) { // sanity 00533 $this->setLastEdit( $revision ); 00534 } 00535 } 00536 00540 protected function setLastEdit( Revision $revision ) { 00541 $this->mLastRevision = $revision; 00542 $this->mTimestamp = $revision->getTimestamp(); 00543 } 00544 00549 public function getRevision() { 00550 $this->loadLastEdit(); 00551 if ( $this->mLastRevision ) { 00552 return $this->mLastRevision; 00553 } 00554 return null; 00555 } 00556 00566 public function getText( $audience = Revision::FOR_PUBLIC ) { 00567 $this->loadLastEdit(); 00568 if ( $this->mLastRevision ) { 00569 return $this->mLastRevision->getText( $audience ); 00570 } 00571 return false; 00572 } 00573 00579 public function getRawText() { 00580 $this->loadLastEdit(); 00581 if ( $this->mLastRevision ) { 00582 return $this->mLastRevision->getRawText(); 00583 } 00584 return false; 00585 } 00586 00590 public function getTimestamp() { 00591 // Check if the field has been filled by WikiPage::setTimestamp() 00592 if ( !$this->mTimestamp ) { 00593 $this->loadLastEdit(); 00594 } 00595 00596 return wfTimestamp( TS_MW, $this->mTimestamp ); 00597 } 00598 00604 public function setTimestamp( $ts ) { 00605 $this->mTimestamp = wfTimestamp( TS_MW, $ts ); 00606 } 00607 00615 public function getUser( $audience = Revision::FOR_PUBLIC ) { 00616 $this->loadLastEdit(); 00617 if ( $this->mLastRevision ) { 00618 return $this->mLastRevision->getUser( $audience ); 00619 } else { 00620 return -1; 00621 } 00622 } 00623 00632 public function getCreator( $audience = Revision::FOR_PUBLIC ) { 00633 $revision = $this->getOldestRevision(); 00634 if ( $revision ) { 00635 $userName = $revision->getUserText( $audience ); 00636 return User::newFromName( $userName, false ); 00637 } else { 00638 return null; 00639 } 00640 } 00641 00649 public function getUserText( $audience = Revision::FOR_PUBLIC ) { 00650 $this->loadLastEdit(); 00651 if ( $this->mLastRevision ) { 00652 return $this->mLastRevision->getUserText( $audience ); 00653 } else { 00654 return ''; 00655 } 00656 } 00657 00665 public function getComment( $audience = Revision::FOR_PUBLIC ) { 00666 $this->loadLastEdit(); 00667 if ( $this->mLastRevision ) { 00668 return $this->mLastRevision->getComment( $audience ); 00669 } else { 00670 return ''; 00671 } 00672 } 00673 00679 public function getMinorEdit() { 00680 $this->loadLastEdit(); 00681 if ( $this->mLastRevision ) { 00682 return $this->mLastRevision->isMinor(); 00683 } else { 00684 return false; 00685 } 00686 } 00687 00693 protected function getCachedLastEditTime() { 00694 global $wgMemc; 00695 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00696 return $wgMemc->get( $key ); 00697 } 00698 00705 public function setCachedLastEditTime( $timestamp ) { 00706 global $wgMemc; 00707 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00708 $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60*15 ); 00709 } 00710 00719 public function isCountable( $editInfo = false ) { 00720 global $wgArticleCountMethod; 00721 00722 if ( !$this->mTitle->isContentPage() ) { 00723 return false; 00724 } 00725 00726 $text = $editInfo ? $editInfo->pst : false; 00727 00728 if ( $this->isRedirect( $text ) ) { 00729 return false; 00730 } 00731 00732 switch ( $wgArticleCountMethod ) { 00733 case 'any': 00734 return true; 00735 case 'comma': 00736 if ( $text === false ) { 00737 $text = $this->getRawText(); 00738 } 00739 return strpos( $text, ',' ) !== false; 00740 case 'link': 00741 if ( $editInfo ) { 00742 // ParserOutput::getLinks() is a 2D array of page links, so 00743 // to be really correct we would need to recurse in the array 00744 // but the main array should only have items in it if there are 00745 // links. 00746 return (bool)count( $editInfo->output->getLinks() ); 00747 } else { 00748 return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, 00749 array( 'pl_from' => $this->getId() ), __METHOD__ ); 00750 } 00751 } 00752 } 00753 00761 public function getRedirectTarget() { 00762 if ( !$this->mTitle->isRedirect() ) { 00763 return null; 00764 } 00765 00766 if ( $this->mRedirectTarget !== null ) { 00767 return $this->mRedirectTarget; 00768 } 00769 00770 # Query the redirect table 00771 $dbr = wfGetDB( DB_SLAVE ); 00772 $row = $dbr->selectRow( 'redirect', 00773 array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), 00774 array( 'rd_from' => $this->getId() ), 00775 __METHOD__ 00776 ); 00777 00778 // rd_fragment and rd_interwiki were added later, populate them if empty 00779 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { 00780 return $this->mRedirectTarget = Title::makeTitle( 00781 $row->rd_namespace, $row->rd_title, 00782 $row->rd_fragment, $row->rd_interwiki ); 00783 } 00784 00785 # This page doesn't have an entry in the redirect table 00786 return $this->mRedirectTarget = $this->insertRedirect(); 00787 } 00788 00795 public function insertRedirect() { 00796 // recurse through to only get the final target 00797 $retval = Title::newFromRedirectRecurse( $this->getRawText() ); 00798 if ( !$retval ) { 00799 return null; 00800 } 00801 $this->insertRedirectEntry( $retval ); 00802 return $retval; 00803 } 00804 00810 public function insertRedirectEntry( $rt ) { 00811 $dbw = wfGetDB( DB_MASTER ); 00812 $dbw->replace( 'redirect', array( 'rd_from' ), 00813 array( 00814 'rd_from' => $this->getId(), 00815 'rd_namespace' => $rt->getNamespace(), 00816 'rd_title' => $rt->getDBkey(), 00817 'rd_fragment' => $rt->getFragment(), 00818 'rd_interwiki' => $rt->getInterwiki(), 00819 ), 00820 __METHOD__ 00821 ); 00822 } 00823 00829 public function followRedirect() { 00830 return $this->getRedirectURL( $this->getRedirectTarget() ); 00831 } 00832 00840 public function getRedirectURL( $rt ) { 00841 if ( !$rt ) { 00842 return false; 00843 } 00844 00845 if ( $rt->isExternal() ) { 00846 if ( $rt->isLocal() ) { 00847 // Offsite wikis need an HTTP redirect. 00848 // 00849 // This can be hard to reverse and may produce loops, 00850 // so they may be disabled in the site configuration. 00851 $source = $this->mTitle->getFullURL( 'redirect=no' ); 00852 return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); 00853 } else { 00854 // External pages pages without "local" bit set are not valid 00855 // redirect targets 00856 return false; 00857 } 00858 } 00859 00860 if ( $rt->isSpecialPage() ) { 00861 // Gotta handle redirects to special pages differently: 00862 // Fill the HTTP response "Location" header and ignore 00863 // the rest of the page we're on. 00864 // 00865 // Some pages are not valid targets 00866 if ( $rt->isValidRedirectTarget() ) { 00867 return $rt->getFullURL(); 00868 } else { 00869 return false; 00870 } 00871 } 00872 00873 return $rt; 00874 } 00875 00881 public function getContributors() { 00882 # @todo FIXME: This is expensive; cache this info somewhere. 00883 00884 $dbr = wfGetDB( DB_SLAVE ); 00885 00886 if ( $dbr->implicitGroupby() ) { 00887 $realNameField = 'user_real_name'; 00888 } else { 00889 $realNameField = 'MIN(user_real_name) AS user_real_name'; 00890 } 00891 00892 $tables = array( 'revision', 'user' ); 00893 00894 $fields = array( 00895 'user_id' => 'rev_user', 00896 'user_name' => 'rev_user_text', 00897 $realNameField, 00898 'timestamp' => 'MAX(rev_timestamp)', 00899 ); 00900 00901 $conds = array( 'rev_page' => $this->getId() ); 00902 00903 // The user who made the top revision gets credited as "this page was last edited by 00904 // John, based on contributions by Tom, Dick and Harry", so don't include them twice. 00905 $user = $this->getUser(); 00906 if ( $user ) { 00907 $conds[] = "rev_user != $user"; 00908 } else { 00909 $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}"; 00910 } 00911 00912 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden? 00913 00914 $jconds = array( 00915 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ), 00916 ); 00917 00918 $options = array( 00919 'GROUP BY' => array( 'rev_user', 'rev_user_text' ), 00920 'ORDER BY' => 'timestamp DESC', 00921 ); 00922 00923 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds ); 00924 return new UserArrayFromResult( $res ); 00925 } 00926 00933 public function getLastNAuthors( $num, $revLatest = 0 ) { 00934 wfProfileIn( __METHOD__ ); 00935 // First try the slave 00936 // If that doesn't have the latest revision, try the master 00937 $continue = 2; 00938 $db = wfGetDB( DB_SLAVE ); 00939 00940 do { 00941 $res = $db->select( array( 'page', 'revision' ), 00942 array( 'rev_id', 'rev_user_text' ), 00943 array( 00944 'page_namespace' => $this->mTitle->getNamespace(), 00945 'page_title' => $this->mTitle->getDBkey(), 00946 'rev_page = page_id' 00947 ), __METHOD__, 00948 array( 00949 'ORDER BY' => 'rev_timestamp DESC', 00950 'LIMIT' => $num 00951 ) 00952 ); 00953 00954 if ( !$res ) { 00955 wfProfileOut( __METHOD__ ); 00956 return array(); 00957 } 00958 00959 $row = $db->fetchObject( $res ); 00960 00961 if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { 00962 $db = wfGetDB( DB_MASTER ); 00963 $continue--; 00964 } else { 00965 $continue = 0; 00966 } 00967 } while ( $continue ); 00968 00969 $authors = array( $row->rev_user_text ); 00970 00971 foreach ( $res as $row ) { 00972 $authors[] = $row->rev_user_text; 00973 } 00974 00975 wfProfileOut( __METHOD__ ); 00976 return $authors; 00977 } 00978 00986 public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { 00987 global $wgEnableParserCache; 00988 00989 return $wgEnableParserCache 00990 && $parserOptions->getStubThreshold() == 0 00991 && $this->mTitle->exists() 00992 && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) 00993 && $this->mTitle->isWikitextPage(); 00994 } 00995 01006 public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { 01007 wfProfileIn( __METHOD__ ); 01008 01009 $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); 01010 wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); 01011 if ( $parserOptions->getStubThreshold() ) { 01012 wfIncrStats( 'pcache_miss_stub' ); 01013 } 01014 01015 if ( $useParserCache ) { 01016 $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); 01017 if ( $parserOutput !== false ) { 01018 wfProfileOut( __METHOD__ ); 01019 return $parserOutput; 01020 } 01021 } 01022 01023 if ( $oldid === null || $oldid === 0 ) { 01024 $oldid = $this->getLatest(); 01025 } 01026 01027 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache ); 01028 $pool->execute(); 01029 01030 wfProfileOut( __METHOD__ ); 01031 01032 return $pool->getParserOutput(); 01033 } 01034 01039 public function doViewUpdates( User $user ) { 01040 global $wgDisableCounters; 01041 if ( wfReadOnly() ) { 01042 return; 01043 } 01044 01045 # Don't update page view counters on views from bot users (bug 14044) 01046 if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->mTitle->exists() ) { 01047 DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); 01048 DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); 01049 } 01050 01051 # Update newtalk / watchlist notification status 01052 $user->clearNotification( $this->mTitle ); 01053 } 01054 01059 public function doPurge() { 01060 global $wgUseSquid; 01061 01062 if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ){ 01063 return false; 01064 } 01065 01066 // Invalidate the cache 01067 $this->mTitle->invalidateCache(); 01068 $this->clear(); 01069 01070 if ( $wgUseSquid ) { 01071 // Commit the transaction before the purge is sent 01072 $dbw = wfGetDB( DB_MASTER ); 01073 $dbw->commit( __METHOD__ ); 01074 01075 // Send purge 01076 $update = SquidUpdate::newSimplePurge( $this->mTitle ); 01077 $update->doUpdate(); 01078 } 01079 01080 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 01081 if ( $this->mTitle->exists() ) { 01082 $text = $this->getRawText(); 01083 } else { 01084 $text = false; 01085 } 01086 01087 MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); 01088 } 01089 return true; 01090 } 01091 01102 public function insertOn( $dbw ) { 01103 wfProfileIn( __METHOD__ ); 01104 01105 $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); 01106 $dbw->insert( 'page', array( 01107 'page_id' => $page_id, 01108 'page_namespace' => $this->mTitle->getNamespace(), 01109 'page_title' => $this->mTitle->getDBkey(), 01110 'page_counter' => 0, 01111 'page_restrictions' => '', 01112 'page_is_redirect' => 0, # Will set this shortly... 01113 'page_is_new' => 1, 01114 'page_random' => wfRandom(), 01115 'page_touched' => $dbw->timestamp(), 01116 'page_latest' => 0, # Fill this in shortly... 01117 'page_len' => 0, # Fill this in shortly... 01118 ), __METHOD__, 'IGNORE' ); 01119 01120 $affected = $dbw->affectedRows(); 01121 01122 if ( $affected ) { 01123 $newid = $dbw->insertId(); 01124 $this->mTitle->resetArticleID( $newid ); 01125 } 01126 wfProfileOut( __METHOD__ ); 01127 01128 return $affected ? $newid : false; 01129 } 01130 01146 public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { 01147 wfProfileIn( __METHOD__ ); 01148 01149 $text = $revision->getText(); 01150 $len = strlen( $text ); 01151 $rt = Title::newFromRedirectRecurse( $text ); 01152 01153 $conditions = array( 'page_id' => $this->getId() ); 01154 01155 if ( !is_null( $lastRevision ) ) { 01156 # An extra check against threads stepping on each other 01157 $conditions['page_latest'] = $lastRevision; 01158 } 01159 01160 $now = wfTimestampNow(); 01161 $dbw->update( 'page', 01162 array( /* SET */ 01163 'page_latest' => $revision->getId(), 01164 'page_touched' => $dbw->timestamp( $now ), 01165 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, 01166 'page_is_redirect' => $rt !== null ? 1 : 0, 01167 'page_len' => $len, 01168 ), 01169 $conditions, 01170 __METHOD__ ); 01171 01172 $result = $dbw->affectedRows() > 0; 01173 if ( $result ) { 01174 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); 01175 $this->setLastEdit( $revision ); 01176 $this->setCachedLastEditTime( $now ); 01177 $this->mLatest = $revision->getId(); 01178 $this->mIsRedirect = (bool)$rt; 01179 # Update the LinkCache. 01180 LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest ); 01181 } 01182 01183 wfProfileOut( __METHOD__ ); 01184 return $result; 01185 } 01186 01198 public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) { 01199 // Always update redirects (target link might have changed) 01200 // Update/Insert if we don't know if the last revision was a redirect or not 01201 // Delete if changing from redirect to non-redirect 01202 $isRedirect = !is_null( $redirectTitle ); 01203 01204 if ( !$isRedirect && $lastRevIsRedirect === false ) { 01205 return true; 01206 } 01207 01208 wfProfileIn( __METHOD__ ); 01209 if ( $isRedirect ) { 01210 $this->insertRedirectEntry( $redirectTitle ); 01211 } else { 01212 // This is not a redirect, remove row from redirect table 01213 $where = array( 'rd_from' => $this->getId() ); 01214 $dbw->delete( 'redirect', $where, __METHOD__ ); 01215 } 01216 01217 if ( $this->getTitle()->getNamespace() == NS_FILE ) { 01218 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); 01219 } 01220 wfProfileOut( __METHOD__ ); 01221 01222 return ( $dbw->affectedRows() != 0 ); 01223 } 01224 01233 public function updateIfNewerOn( $dbw, $revision ) { 01234 wfProfileIn( __METHOD__ ); 01235 01236 $row = $dbw->selectRow( 01237 array( 'revision', 'page' ), 01238 array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), 01239 array( 01240 'page_id' => $this->getId(), 01241 'page_latest=rev_id' ), 01242 __METHOD__ ); 01243 01244 if ( $row ) { 01245 if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { 01246 wfProfileOut( __METHOD__ ); 01247 return false; 01248 } 01249 $prev = $row->rev_id; 01250 $lastRevIsRedirect = (bool)$row->page_is_redirect; 01251 } else { 01252 # No or missing previous revision; mark the page as new 01253 $prev = 0; 01254 $lastRevIsRedirect = null; 01255 } 01256 01257 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); 01258 01259 wfProfileOut( __METHOD__ ); 01260 return $ret; 01261 } 01262 01271 public function getUndoText( Revision $undo, Revision $undoafter = null ) { 01272 $cur_text = $this->getRawText(); 01273 if ( $cur_text === false ) { 01274 return false; // no page 01275 } 01276 $undo_text = $undo->getText(); 01277 $undoafter_text = $undoafter->getText(); 01278 01279 if ( $cur_text == $undo_text ) { 01280 # No use doing a merge if it's just a straight revert. 01281 return $undoafter_text; 01282 } 01283 01284 $undone_text = ''; 01285 01286 if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) { 01287 return false; 01288 } 01289 01290 return $undone_text; 01291 } 01292 01300 public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { 01301 wfProfileIn( __METHOD__ ); 01302 01303 if ( strval( $section ) == '' ) { 01304 // Whole-page edit; let the whole text through 01305 } else { 01306 // Bug 30711: always use current version when adding a new section 01307 if ( is_null( $edittime ) || $section == 'new' ) { 01308 $oldtext = $this->getRawText(); 01309 if ( $oldtext === false ) { 01310 wfDebug( __METHOD__ . ": no page text\n" ); 01311 wfProfileOut( __METHOD__ ); 01312 return null; 01313 } 01314 } else { 01315 $dbw = wfGetDB( DB_MASTER ); 01316 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); 01317 01318 if ( !$rev ) { 01319 wfDebug( "WikiPage::replaceSection asked for bogus section (page: " . 01320 $this->getId() . "; section: $section; edittime: $edittime)\n" ); 01321 wfProfileOut( __METHOD__ ); 01322 return null; 01323 } 01324 01325 $oldtext = $rev->getText(); 01326 } 01327 01328 if ( $section == 'new' ) { 01329 # Inserting a new section 01330 $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' ) 01331 ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : ''; 01332 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) { 01333 $text = strlen( trim( $oldtext ) ) > 0 01334 ? "{$oldtext}\n\n{$subject}{$text}" 01335 : "{$subject}{$text}"; 01336 } 01337 } else { 01338 # Replacing an existing section; roll out the big guns 01339 global $wgParser; 01340 01341 $text = $wgParser->replaceSection( $oldtext, $section, $text ); 01342 } 01343 } 01344 01345 wfProfileOut( __METHOD__ ); 01346 return $text; 01347 } 01348 01354 function checkFlags( $flags ) { 01355 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { 01356 if ( $this->mTitle->getArticleID() ) { 01357 $flags |= EDIT_UPDATE; 01358 } else { 01359 $flags |= EDIT_NEW; 01360 } 01361 } 01362 01363 return $flags; 01364 } 01365 01413 public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { 01414 global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; 01415 01416 # Low-level sanity check 01417 if ( $this->mTitle->getText() === '' ) { 01418 throw new MWException( 'Something is trying to edit an article with an empty title' ); 01419 } 01420 01421 wfProfileIn( __METHOD__ ); 01422 01423 $user = is_null( $user ) ? $wgUser : $user; 01424 $status = Status::newGood( array() ); 01425 01426 // Load the data from the master database if needed. 01427 // The caller may already loaded it from the master or even loaded it using 01428 // SELECT FOR UPDATE, so do not override that using clear(). 01429 $this->loadPageData( 'fromdbmaster' ); 01430 01431 $flags = $this->checkFlags( $flags ); 01432 01433 if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, 01434 $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) 01435 { 01436 wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); 01437 01438 if ( $status->isOK() ) { 01439 $status->fatal( 'edit-hook-aborted' ); 01440 } 01441 01442 wfProfileOut( __METHOD__ ); 01443 return $status; 01444 } 01445 01446 # Silently ignore EDIT_MINOR if not allowed 01447 $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); 01448 $bot = $flags & EDIT_FORCE_BOT; 01449 01450 $oldtext = $this->getRawText(); // current revision 01451 $oldsize = strlen( $oldtext ); 01452 $oldid = $this->getLatest(); 01453 $oldIsRedirect = $this->isRedirect(); 01454 $oldcountable = $this->isCountable(); 01455 01456 # Provide autosummaries if one is not provided and autosummaries are enabled. 01457 if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { 01458 $summary = self::getAutosummary( $oldtext, $text, $flags ); 01459 } 01460 01461 $editInfo = $this->prepareTextForEdit( $text, null, $user ); 01462 $text = $editInfo->pst; 01463 $newsize = strlen( $text ); 01464 01465 $dbw = wfGetDB( DB_MASTER ); 01466 $now = wfTimestampNow(); 01467 $this->mTimestamp = $now; 01468 01469 if ( $flags & EDIT_UPDATE ) { 01470 # Update article, but only if changed. 01471 $status->value['new'] = false; 01472 01473 if ( !$oldid ) { 01474 # Article gone missing 01475 wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); 01476 $status->fatal( 'edit-gone-missing' ); 01477 01478 wfProfileOut( __METHOD__ ); 01479 return $status; 01480 } elseif ( $oldtext === false ) { 01481 # Sanity check for bug 37225 01482 wfProfileOut( __METHOD__ ); 01483 throw new MWException( "Could not find text for current revision {$oldid}." ); 01484 } 01485 01486 $revision = new Revision( array( 01487 'page' => $this->getId(), 01488 'comment' => $summary, 01489 'minor_edit' => $isminor, 01490 'text' => $text, 01491 'parent_id' => $oldid, 01492 'user' => $user->getId(), 01493 'user_text' => $user->getName(), 01494 'timestamp' => $now 01495 ) ); 01496 # Bug 37225: use accessor to get the text as Revision may trim it. 01497 # After trimming, the text may be a duplicate of the current text. 01498 $text = $revision->getText(); // sanity; EditPage should trim already 01499 01500 $changed = ( strcmp( $text, $oldtext ) != 0 ); 01501 01502 if ( $changed ) { 01503 $dbw->begin( __METHOD__ ); 01504 $revisionId = $revision->insertOn( $dbw ); 01505 01506 # Update page 01507 # 01508 # Note that we use $this->mLatest instead of fetching a value from the master DB 01509 # during the course of this function. This makes sure that EditPage can detect 01510 # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() 01511 # before this function is called. A previous function used a separate query, this 01512 # creates a window where concurrent edits can cause an ignored edit conflict. 01513 $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); 01514 01515 if ( !$ok ) { 01516 # Belated edit conflict! Run away!! 01517 $status->fatal( 'edit-conflict' ); 01518 01519 $dbw->rollback( __METHOD__ ); 01520 01521 wfProfileOut( __METHOD__ ); 01522 return $status; 01523 } 01524 01525 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); 01526 # Update recentchanges 01527 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01528 # Mark as patrolled if the user can do so 01529 $patrolled = $wgUseRCPatrol && !count( 01530 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01531 # Add RC row to the DB 01532 $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, 01533 $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, 01534 $revisionId, $patrolled 01535 ); 01536 01537 # Log auto-patrolled edits 01538 if ( $patrolled ) { 01539 PatrolLog::record( $rc, true, $user ); 01540 } 01541 } 01542 $user->incEditCount(); 01543 $dbw->commit( __METHOD__ ); 01544 } else { 01545 // Bug 32948: revision ID must be set to page {{REVISIONID}} and 01546 // related variables correctly 01547 $revision->setId( $this->getLatest() ); 01548 } 01549 01550 # Update links tables, site stats, etc. 01551 $this->doEditUpdates( $revision, $user, array( 'changed' => $changed, 01552 'oldcountable' => $oldcountable ) ); 01553 01554 if ( !$changed ) { 01555 $status->warning( 'edit-no-change' ); 01556 $revision = null; 01557 // Update page_touched, this is usually implicit in the page update 01558 // Other cache updates are done in onArticleEdit() 01559 $this->mTitle->invalidateCache(); 01560 } 01561 } else { 01562 # Create new article 01563 $status->value['new'] = true; 01564 01565 $dbw->begin( __METHOD__ ); 01566 01567 # Add the page record; stake our claim on this title! 01568 # This will return false if the article already exists 01569 $newid = $this->insertOn( $dbw ); 01570 01571 if ( $newid === false ) { 01572 $dbw->rollback( __METHOD__ ); 01573 $status->fatal( 'edit-already-exists' ); 01574 01575 wfProfileOut( __METHOD__ ); 01576 return $status; 01577 } 01578 01579 # Save the revision text... 01580 $revision = new Revision( array( 01581 'page' => $newid, 01582 'comment' => $summary, 01583 'minor_edit' => $isminor, 01584 'text' => $text, 01585 'user' => $user->getId(), 01586 'user_text' => $user->getName(), 01587 'timestamp' => $now 01588 ) ); 01589 $revisionId = $revision->insertOn( $dbw ); 01590 01591 # Bug 37225: use accessor to get the text as Revision may trim it 01592 $text = $revision->getText(); // sanity; EditPage should trim already 01593 01594 # Update the page record with revision data 01595 $this->updateRevisionOn( $dbw, $revision, 0 ); 01596 01597 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 01598 01599 # Update recentchanges 01600 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01601 # Mark as patrolled if the user can do so 01602 $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( 01603 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01604 # Add RC row to the DB 01605 $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, 01606 '', strlen( $text ), $revisionId, $patrolled ); 01607 01608 # Log auto-patrolled edits 01609 if ( $patrolled ) { 01610 PatrolLog::record( $rc, true, $user ); 01611 } 01612 } 01613 $user->incEditCount(); 01614 $dbw->commit( __METHOD__ ); 01615 01616 # Update links, etc. 01617 $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); 01618 01619 wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary, 01620 $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); 01621 } 01622 01623 # Do updates right now unless deferral was requested 01624 if ( !( $flags & EDIT_DEFER_UPDATES ) ) { 01625 DeferredUpdates::doUpdates(); 01626 } 01627 01628 // Return the new revision (or null) to the caller 01629 $status->value['revision'] = $revision; 01630 01631 wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, 01632 $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); 01633 01634 # Promote user to any groups they meet the criteria for 01635 $user->addAutopromoteOnceGroups( 'onEdit' ); 01636 01637 wfProfileOut( __METHOD__ ); 01638 return $status; 01639 } 01640 01653 public function makeParserOptions( $context ) { 01654 global $wgContLang; 01655 01656 if ( $context instanceof IContextSource ) { 01657 $options = ParserOptions::newFromContext( $context ); 01658 } elseif ( $context instanceof User ) { // settings per user (even anons) 01659 $options = ParserOptions::newFromUser( $context ); 01660 } else { // canonical settings 01661 $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); 01662 } 01663 01664 if ( $this->getTitle()->isConversionTable() ) { 01665 $options->disableContentConversion(); 01666 } 01667 01668 $options->enableLimitReport(); // show inclusion/loop reports 01669 $options->setTidy( true ); // fix bad HTML 01670 01671 return $options; 01672 } 01673 01679 public function prepareTextForEdit( $text, $revid = null, User $user = null ) { 01680 global $wgParser, $wgContLang, $wgUser; 01681 $user = is_null( $user ) ? $wgUser : $user; 01682 // @TODO fixme: check $user->getId() here??? 01683 if ( $this->mPreparedEdit 01684 && $this->mPreparedEdit->newText == $text 01685 && $this->mPreparedEdit->revid == $revid 01686 ) { 01687 // Already prepared 01688 return $this->mPreparedEdit; 01689 } 01690 01691 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); 01692 wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) ); 01693 01694 $edit = (object)array(); 01695 $edit->revid = $revid; 01696 $edit->newText = $text; 01697 $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); 01698 $edit->popts = $this->makeParserOptions( 'canonical' ); 01699 $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid ); 01700 $edit->oldText = $this->getRawText(); 01701 01702 $this->mPreparedEdit = $edit; 01703 01704 return $edit; 01705 } 01706 01724 public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { 01725 global $wgEnableParserCache; 01726 01727 wfProfileIn( __METHOD__ ); 01728 01729 $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); 01730 $text = $revision->getText(); 01731 01732 # Parse the text 01733 # Be careful not to double-PST: $text is usually already PST-ed once 01734 if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { 01735 wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); 01736 $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user ); 01737 } else { 01738 wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); 01739 $editInfo = $this->mPreparedEdit; 01740 } 01741 01742 # Save it to the parser cache 01743 if ( $wgEnableParserCache ) { 01744 $parserCache = ParserCache::singleton(); 01745 $parserCache->save( $editInfo->output, $this, $editInfo->popts ); 01746 } 01747 01748 # Update the links tables and other secondary data 01749 $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle ); 01750 DataUpdate::runUpdates( $updates ); 01751 01752 wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); 01753 01754 if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { 01755 if ( 0 == mt_rand( 0, 99 ) ) { 01756 // Flush old entries from the `recentchanges` table; we do this on 01757 // random requests so as to avoid an increase in writes for no good reason 01758 global $wgRCMaxAge; 01759 01760 $dbw = wfGetDB( DB_MASTER ); 01761 $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); 01762 $dbw->delete( 01763 'recentchanges', 01764 array( "rc_timestamp < '$cutoff'" ), 01765 __METHOD__ 01766 ); 01767 } 01768 } 01769 01770 if ( !$this->mTitle->exists() ) { 01771 wfProfileOut( __METHOD__ ); 01772 return; 01773 } 01774 01775 $id = $this->getId(); 01776 $title = $this->mTitle->getPrefixedDBkey(); 01777 $shortTitle = $this->mTitle->getDBkey(); 01778 01779 if ( !$options['changed'] ) { 01780 $good = 0; 01781 $total = 0; 01782 } elseif ( $options['created'] ) { 01783 $good = (int)$this->isCountable( $editInfo ); 01784 $total = 1; 01785 } elseif ( $options['oldcountable'] !== null ) { 01786 $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; 01787 $total = 0; 01788 } else { 01789 $good = 0; 01790 $total = 0; 01791 } 01792 01793 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); 01794 DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) ); 01795 01796 # If this is another user's talk page, update newtalk. 01797 # Don't do this if $options['changed'] = false (null-edits) nor if 01798 # it's a minor edit and the user doesn't want notifications for those. 01799 if ( $options['changed'] 01800 && $this->mTitle->getNamespace() == NS_USER_TALK 01801 && $shortTitle != $user->getTitleKey() 01802 && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) 01803 ) { 01804 if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) { 01805 $other = User::newFromName( $shortTitle, false ); 01806 if ( !$other ) { 01807 wfDebug( __METHOD__ . ": invalid username\n" ); 01808 } elseif ( User::isIP( $shortTitle ) ) { 01809 // An anonymous user 01810 $other->setNewtalk( true, $revision ); 01811 } elseif ( $other->isLoggedIn() ) { 01812 $other->setNewtalk( true, $revision ); 01813 } else { 01814 wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); 01815 } 01816 } 01817 } 01818 01819 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 01820 MessageCache::singleton()->replace( $shortTitle, $text ); 01821 } 01822 01823 if( $options['created'] ) { 01824 self::onArticleCreate( $this->mTitle ); 01825 } else { 01826 self::onArticleEdit( $this->mTitle ); 01827 } 01828 01829 wfProfileOut( __METHOD__ ); 01830 } 01831 01842 public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { 01843 wfProfileIn( __METHOD__ ); 01844 01845 $dbw = wfGetDB( DB_MASTER ); 01846 $revision = new Revision( array( 01847 'page' => $this->getId(), 01848 'text' => $text, 01849 'comment' => $comment, 01850 'minor_edit' => $minor ? 1 : 0, 01851 ) ); 01852 $revision->insertOn( $dbw ); 01853 $this->updateRevisionOn( $dbw, $revision ); 01854 01855 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 01856 01857 wfProfileOut( __METHOD__ ); 01858 } 01859 01871 public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) { 01872 global $wgContLang; 01873 01874 if ( wfReadOnly() ) { 01875 return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); 01876 } 01877 01878 $restrictionTypes = $this->mTitle->getRestrictionTypes(); 01879 01880 $id = $this->mTitle->getArticleID(); 01881 01882 if ( !$cascade ) { 01883 $cascade = false; 01884 } 01885 01886 // Take this opportunity to purge out expired restrictions 01887 Title::purgeExpiredRestrictions(); 01888 01889 # @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); 01890 # we expect a single selection, but the schema allows otherwise. 01891 $isProtected = false; 01892 $protect = false; 01893 $changed = false; 01894 01895 $dbw = wfGetDB( DB_MASTER ); 01896 01897 foreach ( $restrictionTypes as $action ) { 01898 if ( !isset( $expiry[$action] ) ) { 01899 $expiry[$action] = $dbw->getInfinity(); 01900 } 01901 if ( !isset( $limit[$action] ) ) { 01902 $limit[$action] = ''; 01903 } elseif ( $limit[$action] != '' ) { 01904 $protect = true; 01905 } 01906 01907 # Get current restrictions on $action 01908 $current = implode( '', $this->mTitle->getRestrictions( $action ) ); 01909 if ( $current != '' ) { 01910 $isProtected = true; 01911 } 01912 01913 if ( $limit[$action] != $current ) { 01914 $changed = true; 01915 } elseif ( $limit[$action] != '' ) { 01916 # Only check expiry change if the action is actually being 01917 # protected, since expiry does nothing on an not-protected 01918 # action. 01919 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { 01920 $changed = true; 01921 } 01922 } 01923 } 01924 01925 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) { 01926 $changed = true; 01927 } 01928 01929 # If nothing's changed, do nothing 01930 if ( !$changed ) { 01931 return Status::newGood(); 01932 } 01933 01934 if ( !$protect ) { # No protection at all means unprotection 01935 $revCommentMsg = 'unprotectedarticle'; 01936 $logAction = 'unprotect'; 01937 } elseif ( $isProtected ) { 01938 $revCommentMsg = 'modifiedarticleprotection'; 01939 $logAction = 'modify'; 01940 } else { 01941 $revCommentMsg = 'protectedarticle'; 01942 $logAction = 'protect'; 01943 } 01944 01945 $encodedExpiry = array(); 01946 $protectDescription = ''; 01947 foreach ( $limit as $action => $restrictions ) { 01948 $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] ); 01949 if ( $restrictions != '' ) { 01950 $protectDescription .= $wgContLang->getDirMark() . "[$action=$restrictions] ("; 01951 if ( $encodedExpiry[$action] != 'infinity' ) { 01952 $protectDescription .= wfMessage( 01953 'protect-expiring', 01954 $wgContLang->timeanddate( $expiry[$action], false, false ) , 01955 $wgContLang->date( $expiry[$action], false, false ) , 01956 $wgContLang->time( $expiry[$action], false, false ) 01957 )->inContentLanguage()->text(); 01958 } else { 01959 $protectDescription .= wfMessage( 'protect-expiry-indefinite' ) 01960 ->inContentLanguage()->text(); 01961 } 01962 01963 $protectDescription .= ') '; 01964 } 01965 } 01966 $protectDescription = trim( $protectDescription ); 01967 01968 if ( $id ) { # Protection of existing page 01969 if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { 01970 return Status::newGood(); 01971 } 01972 01973 # Only restrictions with the 'protect' right can cascade... 01974 # Otherwise, people who cannot normally protect can "protect" pages via transclusion 01975 $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); 01976 01977 # The schema allows multiple restrictions 01978 if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) { 01979 $cascade = false; 01980 } 01981 01982 # Update restrictions table 01983 foreach ( $limit as $action => $restrictions ) { 01984 if ( $restrictions != '' ) { 01985 $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ), 01986 array( 'pr_page' => $id, 01987 'pr_type' => $action, 01988 'pr_level' => $restrictions, 01989 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, 01990 'pr_expiry' => $encodedExpiry[$action] 01991 ), 01992 __METHOD__ 01993 ); 01994 } else { 01995 $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 01996 'pr_type' => $action ), __METHOD__ ); 01997 } 01998 } 01999 02000 # Prepare a null revision to be added to the history 02001 $editComment = $wgContLang->ucfirst( 02002 wfMessage( 02003 $revCommentMsg, 02004 $this->mTitle->getPrefixedText() 02005 )->inContentLanguage()->text() 02006 ); 02007 if ( $reason ) { 02008 $editComment .= ": $reason"; 02009 } 02010 if ( $protectDescription ) { 02011 $editComment .= " ($protectDescription)"; 02012 } 02013 if ( $cascade ) { 02014 // FIXME: Should use 'brackets' message. 02015 $editComment .= ' [' . wfMessage( 'protect-summary-cascade' ) 02016 ->inContentLanguage()->text() . ']'; 02017 } 02018 02019 # Insert a null revision 02020 $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true ); 02021 $nullRevId = $nullRevision->insertOn( $dbw ); 02022 02023 $latest = $this->getLatest(); 02024 # Update page record 02025 $dbw->update( 'page', 02026 array( /* SET */ 02027 'page_touched' => $dbw->timestamp(), 02028 'page_restrictions' => '', 02029 'page_latest' => $nullRevId 02030 ), array( /* WHERE */ 02031 'page_id' => $id 02032 ), __METHOD__ 02033 ); 02034 02035 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); 02036 wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); 02037 } else { # Protection of non-existing page (also known as "title protection") 02038 # Cascade protection is meaningless in this case 02039 $cascade = false; 02040 02041 if ( $limit['create'] != '' ) { 02042 $dbw->replace( 'protected_titles', 02043 array( array( 'pt_namespace', 'pt_title' ) ), 02044 array( 02045 'pt_namespace' => $this->mTitle->getNamespace(), 02046 'pt_title' => $this->mTitle->getDBkey(), 02047 'pt_create_perm' => $limit['create'], 02048 'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ), 02049 'pt_expiry' => $encodedExpiry['create'], 02050 'pt_user' => $user->getId(), 02051 'pt_reason' => $reason, 02052 ), __METHOD__ 02053 ); 02054 } else { 02055 $dbw->delete( 'protected_titles', 02056 array( 02057 'pt_namespace' => $this->mTitle->getNamespace(), 02058 'pt_title' => $this->mTitle->getDBkey() 02059 ), __METHOD__ 02060 ); 02061 } 02062 } 02063 02064 $this->mTitle->flushRestrictions(); 02065 02066 if ( $logAction == 'unprotect' ) { 02067 $logParams = array(); 02068 } else { 02069 $logParams = array( $protectDescription, $cascade ? 'cascade' : '' ); 02070 } 02071 02072 # Update the protection log 02073 $log = new LogPage( 'protect' ); 02074 $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user ); 02075 02076 return Status::newGood(); 02077 } 02078 02086 protected static function flattenRestrictions( $limit ) { 02087 if ( !is_array( $limit ) ) { 02088 throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' ); 02089 } 02090 02091 $bits = array(); 02092 ksort( $limit ); 02093 02094 foreach ( $limit as $action => $restrictions ) { 02095 if ( $restrictions != '' ) { 02096 $bits[] = "$action=$restrictions"; 02097 } 02098 } 02099 02100 return implode( ':', $bits ); 02101 } 02102 02119 public function doDeleteArticle( 02120 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02121 ) { 02122 $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ); 02123 return $status->isGood(); 02124 } 02125 02142 public function doDeleteArticleReal( 02143 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02144 ) { 02145 global $wgUser; 02146 02147 wfDebug( __METHOD__ . "\n" ); 02148 02149 $status = Status::newGood(); 02150 02151 if ( $this->mTitle->getDBkey() === '' ) { 02152 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02153 return $status; 02154 } 02155 02156 $user = is_null( $user ) ? $wgUser : $user; 02157 if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) { 02158 if ( $status->isOK() ) { 02159 // Hook aborted but didn't set a fatal status 02160 $status->fatal( 'delete-hook-aborted' ); 02161 } 02162 return $status; 02163 } 02164 02165 if ( $id == 0 ) { 02166 $this->loadPageData( 'forupdate' ); 02167 $id = $this->getID(); 02168 if ( $id == 0 ) { 02169 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02170 return $status; 02171 } 02172 } 02173 02174 // Bitfields to further suppress the content 02175 if ( $suppress ) { 02176 $bitfield = 0; 02177 // This should be 15... 02178 $bitfield |= Revision::DELETED_TEXT; 02179 $bitfield |= Revision::DELETED_COMMENT; 02180 $bitfield |= Revision::DELETED_USER; 02181 $bitfield |= Revision::DELETED_RESTRICTED; 02182 } else { 02183 $bitfield = 'rev_deleted'; 02184 } 02185 02186 $dbw = wfGetDB( DB_MASTER ); 02187 $dbw->begin( __METHOD__ ); 02188 // For now, shunt the revision data into the archive table. 02189 // Text is *not* removed from the text table; bulk storage 02190 // is left intact to avoid breaking block-compression or 02191 // immutable storage schemes. 02192 // 02193 // For backwards compatibility, note that some older archive 02194 // table entries will have ar_text and ar_flags fields still. 02195 // 02196 // In the future, we may keep revisions and mark them with 02197 // the rev_deleted field, which is reserved for this purpose. 02198 $dbw->insertSelect( 'archive', array( 'page', 'revision' ), 02199 array( 02200 'ar_namespace' => 'page_namespace', 02201 'ar_title' => 'page_title', 02202 'ar_comment' => 'rev_comment', 02203 'ar_user' => 'rev_user', 02204 'ar_user_text' => 'rev_user_text', 02205 'ar_timestamp' => 'rev_timestamp', 02206 'ar_minor_edit' => 'rev_minor_edit', 02207 'ar_rev_id' => 'rev_id', 02208 'ar_parent_id' => 'rev_parent_id', 02209 'ar_text_id' => 'rev_text_id', 02210 'ar_text' => '\'\'', // Be explicit to appease 02211 'ar_flags' => '\'\'', // MySQL's "strict mode"... 02212 'ar_len' => 'rev_len', 02213 'ar_page_id' => 'page_id', 02214 'ar_deleted' => $bitfield, 02215 'ar_sha1' => 'rev_sha1' 02216 ), array( 02217 'page_id' => $id, 02218 'page_id = rev_page' 02219 ), __METHOD__ 02220 ); 02221 02222 # Now that it's safely backed up, delete it 02223 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); 02224 $ok = ( $dbw->affectedRows() > 0 ); // getArticleID() uses slave, could be laggy 02225 02226 if ( !$ok ) { 02227 $dbw->rollback( __METHOD__ ); 02228 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02229 return $status; 02230 } 02231 02232 $this->doDeleteUpdates( $id ); 02233 02234 # Log the deletion, if the page was suppressed, log it at Oversight instead 02235 $logtype = $suppress ? 'suppress' : 'delete'; 02236 02237 $logEntry = new ManualLogEntry( $logtype, 'delete' ); 02238 $logEntry->setPerformer( $user ); 02239 $logEntry->setTarget( $this->mTitle ); 02240 $logEntry->setComment( $reason ); 02241 $logid = $logEntry->insert(); 02242 $logEntry->publish( $logid ); 02243 02244 if ( $commit ) { 02245 $dbw->commit( __METHOD__ ); 02246 } 02247 02248 wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) ); 02249 $status->value = $logid; 02250 return $status; 02251 } 02252 02258 public function doDeleteUpdates( $id ) { 02259 # update site status 02260 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); 02261 02262 # remove secondary indexes, etc 02263 $updates = $this->getDeletionUpdates( ); 02264 DataUpdate::runUpdates( $updates ); 02265 02266 # Clear caches 02267 WikiPage::onArticleDelete( $this->mTitle ); 02268 02269 # Reset this object 02270 $this->clear(); 02271 02272 # Clear the cached article id so the interface doesn't act like we exist 02273 $this->mTitle->resetArticleID( 0 ); 02274 } 02275 02276 public function getDeletionUpdates() { 02277 $updates = array( 02278 new LinksDeletionUpdate( $this ), 02279 ); 02280 02281 //@todo: make a hook to add update objects 02282 //NOTE: deletion updates will be determined by the ContentHandler in the future 02283 return $updates; 02284 } 02285 02310 public function doRollback( 02311 $fromP, $summary, $token, $bot, &$resultDetails, User $user 02312 ) { 02313 $resultDetails = null; 02314 02315 # Check permissions 02316 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); 02317 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); 02318 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); 02319 02320 if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { 02321 $errors[] = array( 'sessionfailure' ); 02322 } 02323 02324 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) { 02325 $errors[] = array( 'actionthrottledtext' ); 02326 } 02327 02328 # If there were errors, bail out now 02329 if ( !empty( $errors ) ) { 02330 return $errors; 02331 } 02332 02333 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user ); 02334 } 02335 02352 public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { 02353 global $wgUseRCPatrol, $wgContLang; 02354 02355 $dbw = wfGetDB( DB_MASTER ); 02356 02357 if ( wfReadOnly() ) { 02358 return array( array( 'readonlytext' ) ); 02359 } 02360 02361 # Get the last editor 02362 $current = $this->getRevision(); 02363 if ( is_null( $current ) ) { 02364 # Something wrong... no page? 02365 return array( array( 'notanarticle' ) ); 02366 } 02367 02368 $from = str_replace( '_', ' ', $fromP ); 02369 # User name given should match up with the top revision. 02370 # If the user was deleted then $from should be empty. 02371 if ( $from != $current->getUserText() ) { 02372 $resultDetails = array( 'current' => $current ); 02373 return array( array( 'alreadyrolled', 02374 htmlspecialchars( $this->mTitle->getPrefixedText() ), 02375 htmlspecialchars( $fromP ), 02376 htmlspecialchars( $current->getUserText() ) 02377 ) ); 02378 } 02379 02380 # Get the last edit not by this guy... 02381 # Note: these may not be public values 02382 $user = intval( $current->getRawUser() ); 02383 $user_text = $dbw->addQuotes( $current->getRawUserText() ); 02384 $s = $dbw->selectRow( 'revision', 02385 array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), 02386 array( 'rev_page' => $current->getPage(), 02387 "rev_user != {$user} OR rev_user_text != {$user_text}" 02388 ), __METHOD__, 02389 array( 'USE INDEX' => 'page_timestamp', 02390 'ORDER BY' => 'rev_timestamp DESC' ) 02391 ); 02392 if ( $s === false ) { 02393 # No one else ever edited this page 02394 return array( array( 'cantrollback' ) ); 02395 } elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) { 02396 # Only admins can see this text 02397 return array( array( 'notvisiblerev' ) ); 02398 } 02399 02400 $set = array(); 02401 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { 02402 # Mark all reverted edits as bot 02403 $set['rc_bot'] = 1; 02404 } 02405 02406 if ( $wgUseRCPatrol ) { 02407 # Mark all reverted edits as patrolled 02408 $set['rc_patrolled'] = 1; 02409 } 02410 02411 if ( count( $set ) ) { 02412 $dbw->update( 'recentchanges', $set, 02413 array( /* WHERE */ 02414 'rc_cur_id' => $current->getPage(), 02415 'rc_user_text' => $current->getUserText(), 02416 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), 02417 ), __METHOD__ 02418 ); 02419 } 02420 02421 # Generate the edit summary if necessary 02422 $target = Revision::newFromId( $s->rev_id ); 02423 if ( empty( $summary ) ) { 02424 if ( $from == '' ) { // no public user name 02425 $summary = wfMessage( 'revertpage-nouser' ); 02426 } else { 02427 $summary = wfMessage( 'revertpage' ); 02428 } 02429 } 02430 02431 # Allow the custom summary to use the same args as the default message 02432 $args = array( 02433 $target->getUserText(), $from, $s->rev_id, 02434 $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), 02435 $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) 02436 ); 02437 if( $summary instanceof Message ) { 02438 $summary = $summary->params( $args )->inContentLanguage()->text(); 02439 } else { 02440 $summary = wfMsgReplaceArgs( $summary, $args ); 02441 } 02442 02443 # Truncate for whole multibyte characters. 02444 $summary = $wgContLang->truncate( $summary, 255 ); 02445 02446 # Save 02447 $flags = EDIT_UPDATE; 02448 02449 if ( $guser->isAllowed( 'minoredit' ) ) { 02450 $flags |= EDIT_MINOR; 02451 } 02452 02453 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { 02454 $flags |= EDIT_FORCE_BOT; 02455 } 02456 02457 # Actually store the edit 02458 $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser ); 02459 if ( !empty( $status->value['revision'] ) ) { 02460 $revId = $status->value['revision']->getId(); 02461 } else { 02462 $revId = false; 02463 } 02464 02465 wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) ); 02466 02467 $resultDetails = array( 02468 'summary' => $summary, 02469 'current' => $current, 02470 'target' => $target, 02471 'newid' => $revId 02472 ); 02473 02474 return array(); 02475 } 02476 02488 public static function onArticleCreate( $title ) { 02489 # Update existence markers on article/talk tabs... 02490 if ( $title->isTalkPage() ) { 02491 $other = $title->getSubjectPage(); 02492 } else { 02493 $other = $title->getTalkPage(); 02494 } 02495 02496 $other->invalidateCache(); 02497 $other->purgeSquid(); 02498 02499 $title->touchLinks(); 02500 $title->purgeSquid(); 02501 $title->deleteTitleProtection(); 02502 } 02503 02509 public static function onArticleDelete( $title ) { 02510 # Update existence markers on article/talk tabs... 02511 if ( $title->isTalkPage() ) { 02512 $other = $title->getSubjectPage(); 02513 } else { 02514 $other = $title->getTalkPage(); 02515 } 02516 02517 $other->invalidateCache(); 02518 $other->purgeSquid(); 02519 02520 $title->touchLinks(); 02521 $title->purgeSquid(); 02522 02523 # File cache 02524 HTMLFileCache::clearFileCache( $title ); 02525 02526 # Messages 02527 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 02528 MessageCache::singleton()->replace( $title->getDBkey(), false ); 02529 } 02530 02531 # Images 02532 if ( $title->getNamespace() == NS_FILE ) { 02533 $update = new HTMLCacheUpdate( $title, 'imagelinks' ); 02534 $update->doUpdate(); 02535 } 02536 02537 # User talk pages 02538 if ( $title->getNamespace() == NS_USER_TALK ) { 02539 $user = User::newFromName( $title->getText(), false ); 02540 if ( $user ) { 02541 $user->setNewtalk( false ); 02542 } 02543 } 02544 02545 # Image redirects 02546 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); 02547 } 02548 02555 public static function onArticleEdit( $title ) { 02556 // Invalidate caches of articles which include this page 02557 DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); 02558 02559 02560 // Invalidate the caches of all pages which redirect here 02561 DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); 02562 02563 # Purge squid for this page only 02564 $title->purgeSquid(); 02565 02566 # Clear file cache for this page only 02567 HTMLFileCache::clearFileCache( $title ); 02568 } 02569 02578 public function getHiddenCategories() { 02579 $result = array(); 02580 $id = $this->mTitle->getArticleID(); 02581 02582 if ( $id == 0 ) { 02583 return array(); 02584 } 02585 02586 $dbr = wfGetDB( DB_SLAVE ); 02587 $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), 02588 array( 'cl_to' ), 02589 array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', 02590 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), 02591 __METHOD__ ); 02592 02593 if ( $res !== false ) { 02594 foreach ( $res as $row ) { 02595 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); 02596 } 02597 } 02598 02599 return $result; 02600 } 02601 02609 public static function getAutosummary( $oldtext, $newtext, $flags ) { 02610 global $wgContLang; 02611 02612 # Decide what kind of autosummary is needed. 02613 02614 # Redirect autosummaries 02615 $ot = Title::newFromRedirect( $oldtext ); 02616 $rt = Title::newFromRedirect( $newtext ); 02617 02618 if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { 02619 $truncatedtext = $wgContLang->truncate( 02620 str_replace( "\n", ' ', $newtext ), 02621 max( 0, 255 02622 - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) 02623 - strlen( $rt->getFullText() ) 02624 ) ); 02625 return wfMessage( 'autoredircomment', $rt->getFullText() ) 02626 ->rawParams( $truncatedtext )->inContentLanguage()->text(); 02627 } 02628 02629 # New page autosummaries 02630 if ( $flags & EDIT_NEW && strlen( $newtext ) ) { 02631 # If they're making a new article, give its text, truncated, in the summary. 02632 02633 $truncatedtext = $wgContLang->truncate( 02634 str_replace( "\n", ' ', $newtext ), 02635 max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) ); 02636 02637 return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) 02638 ->inContentLanguage()->text(); 02639 } 02640 02641 # Blanking autosummaries 02642 if ( $oldtext != '' && $newtext == '' ) { 02643 return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); 02644 } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) { 02645 # Removing more than 90% of the article 02646 02647 $truncatedtext = $wgContLang->truncate( 02648 $newtext, 02649 max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) ); 02650 02651 return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) 02652 ->inContentLanguage()->text(); 02653 } 02654 02655 # If we reach this point, there's no applicable autosummary for our case, so our 02656 # autosummary is empty. 02657 return ''; 02658 } 02659 02667 public function getAutoDeleteReason( &$hasHistory ) { 02668 global $wgContLang; 02669 02670 // Get the last revision 02671 $rev = $this->getRevision(); 02672 02673 if ( is_null( $rev ) ) { 02674 return false; 02675 } 02676 02677 // Get the article's contents 02678 $contents = $rev->getText(); 02679 $blank = false; 02680 02681 // If the page is blank, use the text from the previous revision, 02682 // which can only be blank if there's a move/import/protect dummy revision involved 02683 if ( $contents == '' ) { 02684 $prev = $rev->getPrevious(); 02685 02686 if ( $prev ) { 02687 $contents = $prev->getText(); 02688 $blank = true; 02689 } 02690 } 02691 02692 $dbw = wfGetDB( DB_MASTER ); 02693 02694 // Find out if there was only one contributor 02695 // Only scan the last 20 revisions 02696 $res = $dbw->select( 'revision', 'rev_user_text', 02697 array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ), 02698 __METHOD__, 02699 array( 'LIMIT' => 20 ) 02700 ); 02701 02702 if ( $res === false ) { 02703 // This page has no revisions, which is very weird 02704 return false; 02705 } 02706 02707 $hasHistory = ( $res->numRows() > 1 ); 02708 $row = $dbw->fetchObject( $res ); 02709 02710 if ( $row ) { // $row is false if the only contributor is hidden 02711 $onlyAuthor = $row->rev_user_text; 02712 // Try to find a second contributor 02713 foreach ( $res as $row ) { 02714 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 02715 $onlyAuthor = false; 02716 break; 02717 } 02718 } 02719 } else { 02720 $onlyAuthor = false; 02721 } 02722 02723 // Generate the summary with a '$1' placeholder 02724 if ( $blank ) { 02725 // The current revision is blank and the one before is also 02726 // blank. It's just not our lucky day 02727 $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text(); 02728 } else { 02729 if ( $onlyAuthor ) { 02730 $reason = wfMessage( 02731 'excontentauthor', 02732 '$1', 02733 $onlyAuthor 02734 )->inContentLanguage()->text(); 02735 } else { 02736 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text(); 02737 } 02738 } 02739 02740 if ( $reason == '-' ) { 02741 // Allow these UI messages to be blanked out cleanly 02742 return ''; 02743 } 02744 02745 // Replace newlines with spaces to prevent uglyness 02746 $contents = preg_replace( "/[\n\r]/", ' ', $contents ); 02747 // Calculate the maximum amount of chars to get 02748 // Max content length = max comment length - length of the comment (excl. $1) 02749 $maxLength = 255 - ( strlen( $reason ) - 2 ); 02750 $contents = $wgContLang->truncate( $contents, $maxLength ); 02751 // Remove possible unfinished links 02752 $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); 02753 // Now replace the '$1' placeholder 02754 $reason = str_replace( '$1', $contents, $reason ); 02755 02756 return $reason; 02757 } 02758 02766 public function updateCategoryCounts( $added, $deleted ) { 02767 $ns = $this->mTitle->getNamespace(); 02768 $dbw = wfGetDB( DB_MASTER ); 02769 02770 # First make sure the rows exist. If one of the "deleted" ones didn't 02771 # exist, we might legitimately not create it, but it's simpler to just 02772 # create it and then give it a negative value, since the value is bogus 02773 # anyway. 02774 # 02775 # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. 02776 $insertCats = array_merge( $added, $deleted ); 02777 if ( !$insertCats ) { 02778 # Okay, nothing to do 02779 return; 02780 } 02781 02782 $insertRows = array(); 02783 02784 foreach ( $insertCats as $cat ) { 02785 $insertRows[] = array( 02786 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ), 02787 'cat_title' => $cat 02788 ); 02789 } 02790 $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); 02791 02792 $addFields = array( 'cat_pages = cat_pages + 1' ); 02793 $removeFields = array( 'cat_pages = cat_pages - 1' ); 02794 02795 if ( $ns == NS_CATEGORY ) { 02796 $addFields[] = 'cat_subcats = cat_subcats + 1'; 02797 $removeFields[] = 'cat_subcats = cat_subcats - 1'; 02798 } elseif ( $ns == NS_FILE ) { 02799 $addFields[] = 'cat_files = cat_files + 1'; 02800 $removeFields[] = 'cat_files = cat_files - 1'; 02801 } 02802 02803 if ( $added ) { 02804 $dbw->update( 02805 'category', 02806 $addFields, 02807 array( 'cat_title' => $added ), 02808 __METHOD__ 02809 ); 02810 } 02811 02812 if ( $deleted ) { 02813 $dbw->update( 02814 'category', 02815 $removeFields, 02816 array( 'cat_title' => $deleted ), 02817 __METHOD__ 02818 ); 02819 } 02820 } 02821 02827 public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) { 02828 if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { 02829 return; 02830 } 02831 02832 // templatelinks table may have become out of sync, 02833 // especially if using variable-based transclusions. 02834 // For paranoia, check if things have changed and if 02835 // so apply updates to the database. This will ensure 02836 // that cascaded protections apply as soon as the changes 02837 // are visible. 02838 02839 # Get templates from templatelinks 02840 $id = $this->mTitle->getArticleID(); 02841 02842 $tlTemplates = array(); 02843 02844 $dbr = wfGetDB( DB_SLAVE ); 02845 $res = $dbr->select( array( 'templatelinks' ), 02846 array( 'tl_namespace', 'tl_title' ), 02847 array( 'tl_from' => $id ), 02848 __METHOD__ 02849 ); 02850 02851 foreach ( $res as $row ) { 02852 $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; 02853 } 02854 02855 # Get templates from parser output. 02856 $poTemplates = array(); 02857 foreach ( $parserOutput->getTemplates() as $ns => $templates ) { 02858 foreach ( $templates as $dbk => $id ) { 02859 $poTemplates["$ns:$dbk"] = true; 02860 } 02861 } 02862 02863 # Get the diff 02864 $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); 02865 02866 if ( count( $templates_diff ) > 0 ) { 02867 # Whee, link updates time. 02868 # Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output. 02869 $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); 02870 $u->doUpdate(); 02871 } 02872 } 02873 02881 public function getUsedTemplates() { 02882 return $this->mTitle->getTemplateLinksFrom(); 02883 } 02884 02895 public function createUpdates( $rev ) { 02896 wfDeprecated( __METHOD__, '1.18' ); 02897 global $wgUser; 02898 $this->doEditUpdates( $rev, $wgUser, array( 'created' => true ) ); 02899 } 02900 02913 public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) { 02914 global $wgParser, $wgUser; 02915 02916 wfDeprecated( __METHOD__, '1.19' ); 02917 02918 $user = is_null( $user ) ? $wgUser : $user; 02919 02920 if ( $popts === null ) { 02921 $popts = ParserOptions::newFromUser( $user ); 02922 } 02923 02924 return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); 02925 } 02926 02933 public function isBigDeletion() { 02934 wfDeprecated( __METHOD__, '1.19' ); 02935 return $this->mTitle->isBigDeletion(); 02936 } 02937 02944 public function estimateRevisionCount() { 02945 wfDeprecated( __METHOD__, '1.19' ); 02946 return $this->mTitle->estimateRevisionCount(); 02947 } 02948 02960 public function updateRestrictions( 02961 $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null 02962 ) { 02963 global $wgUser; 02964 02965 $user = is_null( $user ) ? $wgUser : $user; 02966 02967 return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK(); 02968 } 02969 02973 public function quickEdit( $text, $comment = '', $minor = 0 ) { 02974 wfDeprecated( __METHOD__, '1.18' ); 02975 global $wgUser; 02976 $this->doQuickEdit( $text, $wgUser, $comment, $minor ); 02977 } 02978 02982 public function viewUpdates() { 02983 wfDeprecated( __METHOD__, '1.18' ); 02984 global $wgUser; 02985 return $this->doViewUpdates( $wgUser ); 02986 } 02987 02993 public function useParserCache( $oldid ) { 02994 wfDeprecated( __METHOD__, '1.18' ); 02995 global $wgUser; 02996 return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid ); 02997 } 02998 } 02999 03000 class PoolWorkArticleView extends PoolCounterWork { 03001 03005 private $page; 03006 03010 private $cacheKey; 03011 03015 private $revid; 03016 03020 private $parserOptions; 03021 03025 private $text; 03026 03030 private $parserOutput = false; 03031 03035 private $isDirty = false; 03036 03040 private $error = false; 03041 03051 function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) { 03052 $this->page = $page; 03053 $this->revid = $revid; 03054 $this->cacheable = $useParserCache; 03055 $this->parserOptions = $parserOptions; 03056 $this->text = $text; 03057 $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); 03058 parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); 03059 } 03060 03066 public function getParserOutput() { 03067 return $this->parserOutput; 03068 } 03069 03075 public function getIsDirty() { 03076 return $this->isDirty; 03077 } 03078 03084 public function getError() { 03085 return $this->error; 03086 } 03087 03091 function doWork() { 03092 global $wgParser, $wgUseFileCache; 03093 03094 $isCurrent = $this->revid === $this->page->getLatest(); 03095 03096 if ( $this->text !== null ) { 03097 $text = $this->text; 03098 } elseif ( $isCurrent ) { 03099 $text = $this->page->getRawText(); 03100 } else { 03101 $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); 03102 if ( $rev === null ) { 03103 return false; 03104 } 03105 $text = $rev->getText(); 03106 } 03107 03108 $time = - microtime( true ); 03109 $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(), 03110 $this->parserOptions, true, true, $this->revid ); 03111 $time += microtime( true ); 03112 03113 # Timing hack 03114 if ( $time > 3 ) { 03115 wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, 03116 $this->page->getTitle()->getPrefixedDBkey() ) ); 03117 } 03118 03119 if ( $this->cacheable && $this->parserOutput->isCacheable() ) { 03120 ParserCache::singleton()->save( $this->parserOutput, $this->page, $this->parserOptions ); 03121 } 03122 03123 // Make sure file cache is not used on uncacheable content. 03124 // Output that has magic words in it can still use the parser cache 03125 // (if enabled), though it will generally expire sooner. 03126 if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) { 03127 $wgUseFileCache = false; 03128 } 03129 03130 if ( $isCurrent ) { 03131 $this->page->doCascadeProtectionUpdates( $this->parserOutput ); 03132 } 03133 03134 return true; 03135 } 03136 03140 function getCachedWork() { 03141 $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions ); 03142 03143 if ( $this->parserOutput === false ) { 03144 wfDebug( __METHOD__ . ": parser cache miss\n" ); 03145 return false; 03146 } else { 03147 wfDebug( __METHOD__ . ": parser cache hit\n" ); 03148 return true; 03149 } 03150 } 03151 03155 function fallback() { 03156 $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions ); 03157 03158 if ( $this->parserOutput === false ) { 03159 wfDebugLog( 'dirty', "dirty missing\n" ); 03160 wfDebug( __METHOD__ . ": no dirty cache\n" ); 03161 return false; 03162 } else { 03163 wfDebug( __METHOD__ . ": sending dirty output\n" ); 03164 wfDebugLog( 'dirty', "dirty output {$this->cacheKey}\n" ); 03165 $this->isDirty = true; 03166 return true; 03167 } 03168 } 03169 03174 function error( $status ) { 03175 $this->error = $status; 03176 return false; 03177 } 03178 }