MediaWiki
REL1_19
|
00001 <?php 00005 abstract class Page {} 00006 00015 class WikiPage extends Page { 00016 // doDeleteArticleReal() return values. Values less than zero indicate fatal errors, 00017 // values greater than zero indicate that there were problems not resulting in page 00018 // not being deleted 00019 00023 const DELETE_HOOK_ABORTED = -1; 00024 00028 const DELETE_SUCCESS = 0; 00029 00033 const DELETE_NO_PAGE = 1; 00034 00038 const DELETE_NO_REVISIONS = 2; 00039 00043 public $mTitle = null; 00044 00048 public $mDataLoaded = false; // !< Boolean 00049 public $mIsRedirect = false; // !< Boolean 00050 public $mLatest = false; // !< Integer (false means "not loaded") 00051 public $mPreparedEdit = false; // !< Array 00057 protected $mRedirectTarget = null; 00058 00062 protected $mLastRevision = null; 00063 00067 protected $mTimestamp = ''; 00068 00072 protected $mTouched = '19700101000000'; 00073 00077 protected $mCounter = null; 00078 00083 public function __construct( Title $title ) { 00084 $this->mTitle = $title; 00085 } 00086 00093 public static function factory( Title $title ) { 00094 $ns = $title->getNamespace(); 00095 00096 if ( $ns == NS_MEDIA ) { 00097 throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." ); 00098 } elseif ( $ns < 0 ) { 00099 throw new MWException( "Invalid or virtual namespace $ns given." ); 00100 } 00101 00102 switch ( $ns ) { 00103 case NS_FILE: 00104 $page = new WikiFilePage( $title ); 00105 break; 00106 case NS_CATEGORY: 00107 $page = new WikiCategoryPage( $title ); 00108 break; 00109 default: 00110 $page = new WikiPage( $title ); 00111 } 00112 00113 return $page; 00114 } 00115 00123 public static function newFromID( $id ) { 00124 $t = Title::newFromID( $id ); 00125 if ( $t ) { 00126 return self::factory( $t ); 00127 } 00128 return null; 00129 } 00130 00141 public function getActionOverrides() { 00142 return array(); 00143 } 00144 00149 public function getTitle() { 00150 return $this->mTitle; 00151 } 00152 00156 public function clear() { 00157 $this->mDataLoaded = false; 00158 00159 $this->mCounter = null; 00160 $this->mRedirectTarget = null; # Title object if set 00161 $this->mLastRevision = null; # Latest revision 00162 $this->mTouched = '19700101000000'; 00163 $this->mTimestamp = ''; 00164 $this->mIsRedirect = false; 00165 $this->mLatest = false; 00166 $this->mPreparedEdit = false; 00167 } 00168 00175 public static function selectFields() { 00176 return array( 00177 'page_id', 00178 'page_namespace', 00179 'page_title', 00180 'page_restrictions', 00181 'page_counter', 00182 'page_is_redirect', 00183 'page_is_new', 00184 'page_random', 00185 'page_touched', 00186 'page_latest', 00187 'page_len', 00188 ); 00189 } 00190 00197 protected function pageData( $dbr, $conditions ) { 00198 $fields = self::selectFields(); 00199 00200 wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); 00201 00202 $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__ ); 00203 00204 wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); 00205 00206 return $row; 00207 } 00208 00217 public function pageDataFromTitle( $dbr, $title ) { 00218 return $this->pageData( $dbr, array( 00219 'page_namespace' => $title->getNamespace(), 00220 'page_title' => $title->getDBkey() ) ); 00221 } 00222 00230 public function pageDataFromId( $dbr, $id ) { 00231 return $this->pageData( $dbr, array( 'page_id' => $id ) ); 00232 } 00233 00244 public function loadPageData( $data = 'fromdb' ) { 00245 if ( $data === 'fromdbmaster' ) { 00246 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00247 } elseif ( $data === 'fromdb' ) { // slave 00248 $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); 00249 # Use a "last rev inserted" timestamp key to dimish the issue of slave lag. 00250 # Note that DB also stores the master position in the session and checks it. 00251 $touched = $this->getCachedLastEditTime(); 00252 if ( $touched ) { // key set 00253 if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { 00254 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00255 } 00256 } 00257 } 00258 00259 $lc = LinkCache::singleton(); 00260 00261 if ( $data ) { 00262 $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); 00263 00264 $this->mTitle->loadFromRow( $data ); 00265 00266 # Old-fashioned restrictions 00267 $this->mTitle->loadRestrictions( $data->page_restrictions ); 00268 00269 $this->mCounter = intval( $data->page_counter ); 00270 $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); 00271 $this->mIsRedirect = intval( $data->page_is_redirect ); 00272 $this->mLatest = intval( $data->page_latest ); 00273 } else { 00274 $lc->addBadLinkObj( $this->mTitle ); 00275 00276 $this->mTitle->loadFromRow( false ); 00277 } 00278 00279 $this->mDataLoaded = true; 00280 } 00281 00285 public function getId() { 00286 return $this->mTitle->getArticleID(); 00287 } 00288 00292 public function exists() { 00293 return $this->mTitle->exists(); 00294 } 00295 00304 public function hasViewableContent() { 00305 return $this->mTitle->exists() || $this->mTitle->isAlwaysKnown(); 00306 } 00307 00311 public function getCount() { 00312 if ( !$this->mDataLoaded ) { 00313 $this->loadPageData(); 00314 } 00315 00316 return $this->mCounter; 00317 } 00318 00325 public function isRedirect( $text = false ) { 00326 if ( $text === false ) { 00327 if ( !$this->mDataLoaded ) { 00328 $this->loadPageData(); 00329 } 00330 00331 return (bool)$this->mIsRedirect; 00332 } else { 00333 return Title::newFromRedirect( $text ) !== null; 00334 } 00335 } 00336 00341 public function checkTouched() { 00342 if ( !$this->mDataLoaded ) { 00343 $this->loadPageData(); 00344 } 00345 return !$this->mIsRedirect; 00346 } 00347 00352 public function getTouched() { 00353 if ( !$this->mDataLoaded ) { 00354 $this->loadPageData(); 00355 } 00356 return $this->mTouched; 00357 } 00358 00363 public function getLatest() { 00364 if ( !$this->mDataLoaded ) { 00365 $this->loadPageData(); 00366 } 00367 return (int)$this->mLatest; 00368 } 00369 00374 protected function loadLastEdit() { 00375 if ( $this->mLastRevision !== null ) { 00376 return; // already loaded 00377 } 00378 00379 $latest = $this->getLatest(); 00380 if ( !$latest ) { 00381 return; // page doesn't exist or is missing page_latest info 00382 } 00383 00384 $revision = Revision::newFromPageId( $this->getId(), $latest ); 00385 if ( $revision ) { // sanity 00386 $this->setLastEdit( $revision ); 00387 } 00388 } 00389 00393 protected function setLastEdit( Revision $revision ) { 00394 $this->mLastRevision = $revision; 00395 $this->mTimestamp = $revision->getTimestamp(); 00396 } 00397 00402 public function getRevision() { 00403 $this->loadLastEdit(); 00404 if ( $this->mLastRevision ) { 00405 return $this->mLastRevision; 00406 } 00407 return null; 00408 } 00409 00419 public function getText( $audience = Revision::FOR_PUBLIC ) { 00420 $this->loadLastEdit(); 00421 if ( $this->mLastRevision ) { 00422 return $this->mLastRevision->getText( $audience ); 00423 } 00424 return false; 00425 } 00426 00432 public function getRawText() { 00433 $this->loadLastEdit(); 00434 if ( $this->mLastRevision ) { 00435 return $this->mLastRevision->getRawText(); 00436 } 00437 return false; 00438 } 00439 00443 public function getTimestamp() { 00444 // Check if the field has been filled by WikiPage::setTimestamp() 00445 if ( !$this->mTimestamp ) { 00446 $this->loadLastEdit(); 00447 } 00448 return wfTimestamp( TS_MW, $this->mTimestamp ); 00449 } 00450 00456 public function setTimestamp( $ts ) { 00457 $this->mTimestamp = wfTimestamp( TS_MW, $ts ); 00458 } 00459 00467 public function getUser( $audience = Revision::FOR_PUBLIC ) { 00468 $this->loadLastEdit(); 00469 if ( $this->mLastRevision ) { 00470 return $this->mLastRevision->getUser( $audience ); 00471 } else { 00472 return -1; 00473 } 00474 } 00475 00483 public function getUserText( $audience = Revision::FOR_PUBLIC ) { 00484 $this->loadLastEdit(); 00485 if ( $this->mLastRevision ) { 00486 return $this->mLastRevision->getUserText( $audience ); 00487 } else { 00488 return ''; 00489 } 00490 } 00491 00499 public function getComment( $audience = Revision::FOR_PUBLIC ) { 00500 $this->loadLastEdit(); 00501 if ( $this->mLastRevision ) { 00502 return $this->mLastRevision->getComment( $audience ); 00503 } else { 00504 return ''; 00505 } 00506 } 00507 00513 public function getMinorEdit() { 00514 $this->loadLastEdit(); 00515 if ( $this->mLastRevision ) { 00516 return $this->mLastRevision->isMinor(); 00517 } else { 00518 return false; 00519 } 00520 } 00521 00527 protected function getCachedLastEditTime() { 00528 global $wgMemc; 00529 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00530 return $wgMemc->get( $key ); 00531 } 00532 00539 public function setCachedLastEditTime( $timestamp ) { 00540 global $wgMemc; 00541 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00542 $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60*15 ); 00543 } 00544 00553 public function isCountable( $editInfo = false ) { 00554 global $wgArticleCountMethod; 00555 00556 if ( !$this->mTitle->isContentPage() ) { 00557 return false; 00558 } 00559 00560 $text = $editInfo ? $editInfo->pst : false; 00561 00562 if ( $this->isRedirect( $text ) ) { 00563 return false; 00564 } 00565 00566 switch ( $wgArticleCountMethod ) { 00567 case 'any': 00568 return true; 00569 case 'comma': 00570 if ( $text === false ) { 00571 $text = $this->getRawText(); 00572 } 00573 return strpos( $text, ',' ) !== false; 00574 case 'link': 00575 if ( $editInfo ) { 00576 // ParserOutput::getLinks() is a 2D array of page links, so 00577 // to be really correct we would need to recurse in the array 00578 // but the main array should only have items in it if there are 00579 // links. 00580 return (bool)count( $editInfo->output->getLinks() ); 00581 } else { 00582 return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, 00583 array( 'pl_from' => $this->getId() ), __METHOD__ ); 00584 } 00585 } 00586 } 00587 00595 public function getRedirectTarget() { 00596 if ( !$this->mTitle->isRedirect() ) { 00597 return null; 00598 } 00599 00600 if ( $this->mRedirectTarget !== null ) { 00601 return $this->mRedirectTarget; 00602 } 00603 00604 # Query the redirect table 00605 $dbr = wfGetDB( DB_SLAVE ); 00606 $row = $dbr->selectRow( 'redirect', 00607 array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), 00608 array( 'rd_from' => $this->getId() ), 00609 __METHOD__ 00610 ); 00611 00612 // rd_fragment and rd_interwiki were added later, populate them if empty 00613 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { 00614 return $this->mRedirectTarget = Title::makeTitle( 00615 $row->rd_namespace, $row->rd_title, 00616 $row->rd_fragment, $row->rd_interwiki ); 00617 } 00618 00619 # This page doesn't have an entry in the redirect table 00620 return $this->mRedirectTarget = $this->insertRedirect(); 00621 } 00622 00629 public function insertRedirect() { 00630 // recurse through to only get the final target 00631 $retval = Title::newFromRedirectRecurse( $this->getRawText() ); 00632 if ( !$retval ) { 00633 return null; 00634 } 00635 $this->insertRedirectEntry( $retval ); 00636 return $retval; 00637 } 00638 00644 public function insertRedirectEntry( $rt ) { 00645 $dbw = wfGetDB( DB_MASTER ); 00646 $dbw->replace( 'redirect', array( 'rd_from' ), 00647 array( 00648 'rd_from' => $this->getId(), 00649 'rd_namespace' => $rt->getNamespace(), 00650 'rd_title' => $rt->getDBkey(), 00651 'rd_fragment' => $rt->getFragment(), 00652 'rd_interwiki' => $rt->getInterwiki(), 00653 ), 00654 __METHOD__ 00655 ); 00656 } 00657 00663 public function followRedirect() { 00664 return $this->getRedirectURL( $this->getRedirectTarget() ); 00665 } 00666 00674 public function getRedirectURL( $rt ) { 00675 if ( !$rt ) { 00676 return false; 00677 } 00678 00679 if ( $rt->isExternal() ) { 00680 if ( $rt->isLocal() ) { 00681 // Offsite wikis need an HTTP redirect. 00682 // 00683 // This can be hard to reverse and may produce loops, 00684 // so they may be disabled in the site configuration. 00685 $source = $this->mTitle->getFullURL( 'redirect=no' ); 00686 return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); 00687 } else { 00688 // External pages pages without "local" bit set are not valid 00689 // redirect targets 00690 return false; 00691 } 00692 } 00693 00694 if ( $rt->isSpecialPage() ) { 00695 // Gotta handle redirects to special pages differently: 00696 // Fill the HTTP response "Location" header and ignore 00697 // the rest of the page we're on. 00698 // 00699 // Some pages are not valid targets 00700 if ( $rt->isValidRedirectTarget() ) { 00701 return $rt->getFullURL(); 00702 } else { 00703 return false; 00704 } 00705 } 00706 00707 return $rt; 00708 } 00709 00715 public function getContributors() { 00716 # @todo FIXME: This is expensive; cache this info somewhere. 00717 00718 $dbr = wfGetDB( DB_SLAVE ); 00719 00720 if ( $dbr->implicitGroupby() ) { 00721 $realNameField = 'user_real_name'; 00722 } else { 00723 $realNameField = 'MIN(user_real_name) AS user_real_name'; 00724 } 00725 00726 $tables = array( 'revision', 'user' ); 00727 00728 $fields = array( 00729 'rev_user as user_id', 00730 'rev_user_text AS user_name', 00731 $realNameField, 00732 'MAX(rev_timestamp) AS timestamp', 00733 ); 00734 00735 $conds = array( 'rev_page' => $this->getId() ); 00736 00737 // The user who made the top revision gets credited as "this page was last edited by 00738 // John, based on contributions by Tom, Dick and Harry", so don't include them twice. 00739 $user = $this->getUser(); 00740 if ( $user ) { 00741 $conds[] = "rev_user != $user"; 00742 } else { 00743 $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}"; 00744 } 00745 00746 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden? 00747 00748 $jconds = array( 00749 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ), 00750 ); 00751 00752 $options = array( 00753 'GROUP BY' => array( 'rev_user', 'rev_user_text' ), 00754 'ORDER BY' => 'timestamp DESC', 00755 ); 00756 00757 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds ); 00758 return new UserArrayFromResult( $res ); 00759 } 00760 00767 public function getLastNAuthors( $num, $revLatest = 0 ) { 00768 wfProfileIn( __METHOD__ ); 00769 // First try the slave 00770 // If that doesn't have the latest revision, try the master 00771 $continue = 2; 00772 $db = wfGetDB( DB_SLAVE ); 00773 00774 do { 00775 $res = $db->select( array( 'page', 'revision' ), 00776 array( 'rev_id', 'rev_user_text' ), 00777 array( 00778 'page_namespace' => $this->mTitle->getNamespace(), 00779 'page_title' => $this->mTitle->getDBkey(), 00780 'rev_page = page_id' 00781 ), __METHOD__, 00782 array( 00783 'ORDER BY' => 'rev_timestamp DESC', 00784 'LIMIT' => $num 00785 ) 00786 ); 00787 00788 if ( !$res ) { 00789 wfProfileOut( __METHOD__ ); 00790 return array(); 00791 } 00792 00793 $row = $db->fetchObject( $res ); 00794 00795 if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { 00796 $db = wfGetDB( DB_MASTER ); 00797 $continue--; 00798 } else { 00799 $continue = 0; 00800 } 00801 } while ( $continue ); 00802 00803 $authors = array( $row->rev_user_text ); 00804 00805 foreach ( $res as $row ) { 00806 $authors[] = $row->rev_user_text; 00807 } 00808 00809 wfProfileOut( __METHOD__ ); 00810 return $authors; 00811 } 00812 00820 public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { 00821 global $wgEnableParserCache; 00822 00823 return $wgEnableParserCache 00824 && $parserOptions->getStubThreshold() == 0 00825 && $this->mTitle->exists() 00826 && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) 00827 && $this->mTitle->isWikitextPage(); 00828 } 00829 00840 public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { 00841 wfProfileIn( __METHOD__ ); 00842 00843 $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); 00844 wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); 00845 if ( $parserOptions->getStubThreshold() ) { 00846 wfIncrStats( 'pcache_miss_stub' ); 00847 } 00848 00849 if ( $useParserCache ) { 00850 $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); 00851 if ( $parserOutput !== false ) { 00852 wfProfileOut( __METHOD__ ); 00853 return $parserOutput; 00854 } 00855 } 00856 00857 if ( $oldid === null || $oldid === 0 ) { 00858 $oldid = $this->getLatest(); 00859 } 00860 00861 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache ); 00862 $pool->execute(); 00863 00864 wfProfileOut( __METHOD__ ); 00865 00866 return $pool->getParserOutput(); 00867 } 00868 00873 public function doViewUpdates( User $user ) { 00874 global $wgDisableCounters; 00875 if ( wfReadOnly() ) { 00876 return; 00877 } 00878 00879 # Don't update page view counters on views from bot users (bug 14044) 00880 if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->mTitle->exists() ) { 00881 DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); 00882 DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); 00883 } 00884 00885 # Update newtalk / watchlist notification status 00886 $user->clearNotification( $this->mTitle ); 00887 } 00888 00892 public function doPurge() { 00893 global $wgUseSquid; 00894 00895 if( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ){ 00896 return false; 00897 } 00898 00899 // Invalidate the cache 00900 $this->mTitle->invalidateCache(); 00901 $this->clear(); 00902 00903 if ( $wgUseSquid ) { 00904 // Commit the transaction before the purge is sent 00905 $dbw = wfGetDB( DB_MASTER ); 00906 $dbw->commit(); 00907 00908 // Send purge 00909 $update = SquidUpdate::newSimplePurge( $this->mTitle ); 00910 $update->doUpdate(); 00911 } 00912 00913 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 00914 if ( $this->mTitle->exists() ) { 00915 $text = $this->getRawText(); 00916 } else { 00917 $text = false; 00918 } 00919 00920 MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); 00921 } 00922 return true; 00923 } 00924 00935 public function insertOn( $dbw ) { 00936 wfProfileIn( __METHOD__ ); 00937 00938 $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); 00939 $dbw->insert( 'page', array( 00940 'page_id' => $page_id, 00941 'page_namespace' => $this->mTitle->getNamespace(), 00942 'page_title' => $this->mTitle->getDBkey(), 00943 'page_counter' => 0, 00944 'page_restrictions' => '', 00945 'page_is_redirect' => 0, # Will set this shortly... 00946 'page_is_new' => 1, 00947 'page_random' => wfRandom(), 00948 'page_touched' => $dbw->timestamp(), 00949 'page_latest' => 0, # Fill this in shortly... 00950 'page_len' => 0, # Fill this in shortly... 00951 ), __METHOD__, 'IGNORE' ); 00952 00953 $affected = $dbw->affectedRows(); 00954 00955 if ( $affected ) { 00956 $newid = $dbw->insertId(); 00957 $this->mTitle->resetArticleID( $newid ); 00958 } 00959 wfProfileOut( __METHOD__ ); 00960 00961 return $affected ? $newid : false; 00962 } 00963 00979 public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) { 00980 wfProfileIn( __METHOD__ ); 00981 00982 $text = $revision->getText(); 00983 $len = strlen( $text ); 00984 $rt = Title::newFromRedirectRecurse( $text ); 00985 00986 $conditions = array( 'page_id' => $this->getId() ); 00987 00988 if ( !is_null( $lastRevision ) ) { 00989 # An extra check against threads stepping on each other 00990 $conditions['page_latest'] = $lastRevision; 00991 } 00992 00993 $now = wfTimestampNow(); 00994 $dbw->update( 'page', 00995 array( /* SET */ 00996 'page_latest' => $revision->getId(), 00997 'page_touched' => $dbw->timestamp( $now ), 00998 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, 00999 'page_is_redirect' => $rt !== null ? 1 : 0, 01000 'page_len' => $len, 01001 ), 01002 $conditions, 01003 __METHOD__ ); 01004 01005 $result = $dbw->affectedRows() != 0; 01006 if ( $result ) { 01007 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); 01008 $this->setLastEdit( $revision ); 01009 $this->setCachedLastEditTime( $now ); 01010 $this->mLatest = $revision->getId(); 01011 $this->mIsRedirect = (bool)$rt; 01012 # Update the LinkCache. 01013 LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest ); 01014 } 01015 01016 wfProfileOut( __METHOD__ ); 01017 return $result; 01018 } 01019 01031 public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) { 01032 // Always update redirects (target link might have changed) 01033 // Update/Insert if we don't know if the last revision was a redirect or not 01034 // Delete if changing from redirect to non-redirect 01035 $isRedirect = !is_null( $redirectTitle ); 01036 01037 if ( !$isRedirect && $lastRevIsRedirect === false ) { 01038 return true; 01039 } 01040 01041 wfProfileIn( __METHOD__ ); 01042 if ( $isRedirect ) { 01043 $this->insertRedirectEntry( $redirectTitle ); 01044 } else { 01045 // This is not a redirect, remove row from redirect table 01046 $where = array( 'rd_from' => $this->getId() ); 01047 $dbw->delete( 'redirect', $where, __METHOD__ ); 01048 } 01049 01050 if ( $this->getTitle()->getNamespace() == NS_FILE ) { 01051 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); 01052 } 01053 wfProfileOut( __METHOD__ ); 01054 01055 return ( $dbw->affectedRows() != 0 ); 01056 } 01057 01066 public function updateIfNewerOn( $dbw, $revision ) { 01067 wfProfileIn( __METHOD__ ); 01068 01069 $row = $dbw->selectRow( 01070 array( 'revision', 'page' ), 01071 array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), 01072 array( 01073 'page_id' => $this->getId(), 01074 'page_latest=rev_id' ), 01075 __METHOD__ ); 01076 01077 if ( $row ) { 01078 if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { 01079 wfProfileOut( __METHOD__ ); 01080 return false; 01081 } 01082 $prev = $row->rev_id; 01083 $lastRevIsRedirect = (bool)$row->page_is_redirect; 01084 } else { 01085 # No or missing previous revision; mark the page as new 01086 $prev = 0; 01087 $lastRevIsRedirect = null; 01088 } 01089 01090 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); 01091 01092 wfProfileOut( __METHOD__ ); 01093 return $ret; 01094 } 01095 01104 public function getUndoText( Revision $undo, Revision $undoafter = null ) { 01105 $cur_text = $this->getRawText(); 01106 if ( $cur_text === false ) { 01107 return false; // no page 01108 } 01109 $undo_text = $undo->getText(); 01110 $undoafter_text = $undoafter->getText(); 01111 01112 if ( $cur_text == $undo_text ) { 01113 # No use doing a merge if it's just a straight revert. 01114 return $undoafter_text; 01115 } 01116 01117 $undone_text = ''; 01118 01119 if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) { 01120 return false; 01121 } 01122 01123 return $undone_text; 01124 } 01125 01133 public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { 01134 wfProfileIn( __METHOD__ ); 01135 01136 if ( strval( $section ) == '' ) { 01137 // Whole-page edit; let the whole text through 01138 } else { 01139 // Bug 30711: always use current version when adding a new section 01140 if ( is_null( $edittime ) || $section == 'new' ) { 01141 $oldtext = $this->getRawText(); 01142 if ( $oldtext === false ) { 01143 wfDebug( __METHOD__ . ": no page text\n" ); 01144 wfProfileOut( __METHOD__ ); 01145 return null; 01146 } 01147 } else { 01148 $dbw = wfGetDB( DB_MASTER ); 01149 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); 01150 01151 if ( !$rev ) { 01152 wfDebug( "WikiPage::replaceSection asked for bogus section (page: " . 01153 $this->getId() . "; section: $section; edittime: $edittime)\n" ); 01154 wfProfileOut( __METHOD__ ); 01155 return null; 01156 } 01157 01158 $oldtext = $rev->getText(); 01159 } 01160 01161 if ( $section == 'new' ) { 01162 # Inserting a new section 01163 $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : ''; 01164 if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) { 01165 $text = strlen( trim( $oldtext ) ) > 0 01166 ? "{$oldtext}\n\n{$subject}{$text}" 01167 : "{$subject}{$text}"; 01168 } 01169 } else { 01170 # Replacing an existing section; roll out the big guns 01171 global $wgParser; 01172 01173 $text = $wgParser->replaceSection( $oldtext, $section, $text ); 01174 } 01175 } 01176 01177 wfProfileOut( __METHOD__ ); 01178 return $text; 01179 } 01180 01186 function checkFlags( $flags ) { 01187 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { 01188 if ( $this->mTitle->getArticleID() ) { 01189 $flags |= EDIT_UPDATE; 01190 } else { 01191 $flags |= EDIT_NEW; 01192 } 01193 } 01194 01195 return $flags; 01196 } 01197 01244 public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { 01245 global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries; 01246 01247 # Low-level sanity check 01248 if ( $this->mTitle->getText() === '' ) { 01249 throw new MWException( 'Something is trying to edit an article with an empty title' ); 01250 } 01251 01252 wfProfileIn( __METHOD__ ); 01253 01254 $user = is_null( $user ) ? $wgUser : $user; 01255 $status = Status::newGood( array() ); 01256 01257 # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already 01258 $this->loadPageData( 'fromdbmaster' ); 01259 01260 $flags = $this->checkFlags( $flags ); 01261 01262 if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary, 01263 $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) ) 01264 { 01265 wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); 01266 01267 if ( $status->isOK() ) { 01268 $status->fatal( 'edit-hook-aborted' ); 01269 } 01270 01271 wfProfileOut( __METHOD__ ); 01272 return $status; 01273 } 01274 01275 # Silently ignore EDIT_MINOR if not allowed 01276 $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); 01277 $bot = $flags & EDIT_FORCE_BOT; 01278 01279 $oldtext = $this->getRawText(); // current revision 01280 $oldsize = strlen( $oldtext ); 01281 $oldid = $this->getLatest(); 01282 $oldIsRedirect = $this->isRedirect(); 01283 $oldcountable = $this->isCountable(); 01284 01285 # Provide autosummaries if one is not provided and autosummaries are enabled. 01286 if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { 01287 $summary = self::getAutosummary( $oldtext, $text, $flags ); 01288 } 01289 01290 $editInfo = $this->prepareTextForEdit( $text, null, $user ); 01291 $text = $editInfo->pst; 01292 $newsize = strlen( $text ); 01293 01294 $dbw = wfGetDB( DB_MASTER ); 01295 $now = wfTimestampNow(); 01296 $this->mTimestamp = $now; 01297 01298 if ( $flags & EDIT_UPDATE ) { 01299 # Update article, but only if changed. 01300 $status->value['new'] = false; 01301 01302 if ( !$oldid ) { 01303 # Article gone missing 01304 wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); 01305 $status->fatal( 'edit-gone-missing' ); 01306 01307 wfProfileOut( __METHOD__ ); 01308 return $status; 01309 } 01310 01311 # Make sure the revision is either completely inserted or not inserted at all 01312 if ( !$wgDBtransactions ) { 01313 $userAbort = ignore_user_abort( true ); 01314 } 01315 01316 $revision = new Revision( array( 01317 'page' => $this->getId(), 01318 'comment' => $summary, 01319 'minor_edit' => $isminor, 01320 'text' => $text, 01321 'parent_id' => $oldid, 01322 'user' => $user->getId(), 01323 'user_text' => $user->getName(), 01324 'timestamp' => $now 01325 ) ); 01326 01327 $changed = ( strcmp( $text, $oldtext ) != 0 ); 01328 01329 if ( $changed ) { 01330 $dbw->begin(); 01331 $revisionId = $revision->insertOn( $dbw ); 01332 01333 # Update page 01334 # 01335 # Note that we use $this->mLatest instead of fetching a value from the master DB 01336 # during the course of this function. This makes sure that EditPage can detect 01337 # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() 01338 # before this function is called. A previous function used a separate query, this 01339 # creates a window where concurrent edits can cause an ignored edit conflict. 01340 $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); 01341 01342 if ( !$ok ) { 01343 /* Belated edit conflict! Run away!! */ 01344 $status->fatal( 'edit-conflict' ); 01345 01346 # Delete the invalid revision if the DB is not transactional 01347 if ( !$wgDBtransactions ) { 01348 $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ ); 01349 } 01350 01351 $revisionId = 0; 01352 $dbw->rollback(); 01353 } else { 01354 global $wgUseRCPatrol; 01355 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); 01356 # Update recentchanges 01357 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01358 # Mark as patrolled if the user can do so 01359 $patrolled = $wgUseRCPatrol && !count( 01360 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01361 # Add RC row to the DB 01362 $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, 01363 $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, 01364 $revisionId, $patrolled 01365 ); 01366 01367 # Log auto-patrolled edits 01368 if ( $patrolled ) { 01369 PatrolLog::record( $rc, true ); 01370 } 01371 } 01372 $user->incEditCount(); 01373 $dbw->commit(); 01374 } 01375 } else { 01376 // Bug 32948: revision ID must be set to page {{REVISIONID}} and 01377 // related variables correctly 01378 $revision->setId( $this->getLatest() ); 01379 } 01380 01381 if ( !$wgDBtransactions ) { 01382 ignore_user_abort( $userAbort ); 01383 } 01384 01385 // Now that ignore_user_abort is restored, we can respond to fatal errors 01386 if ( !$status->isOK() ) { 01387 wfProfileOut( __METHOD__ ); 01388 return $status; 01389 } 01390 01391 # Update links tables, site stats, etc. 01392 $this->doEditUpdates( $revision, $user, array( 'changed' => $changed, 01393 'oldcountable' => $oldcountable ) ); 01394 01395 if ( !$changed ) { 01396 $status->warning( 'edit-no-change' ); 01397 $revision = null; 01398 // Update page_touched, this is usually implicit in the page update 01399 // Other cache updates are done in onArticleEdit() 01400 $this->mTitle->invalidateCache(); 01401 } 01402 } else { 01403 # Create new article 01404 $status->value['new'] = true; 01405 01406 $dbw->begin(); 01407 01408 # Add the page record; stake our claim on this title! 01409 # This will return false if the article already exists 01410 $newid = $this->insertOn( $dbw ); 01411 01412 if ( $newid === false ) { 01413 $dbw->rollback(); 01414 $status->fatal( 'edit-already-exists' ); 01415 01416 wfProfileOut( __METHOD__ ); 01417 return $status; 01418 } 01419 01420 # Save the revision text... 01421 $revision = new Revision( array( 01422 'page' => $newid, 01423 'comment' => $summary, 01424 'minor_edit' => $isminor, 01425 'text' => $text, 01426 'user' => $user->getId(), 01427 'user_text' => $user->getName(), 01428 'timestamp' => $now 01429 ) ); 01430 $revisionId = $revision->insertOn( $dbw ); 01431 01432 # Update the page record with revision data 01433 $this->updateRevisionOn( $dbw, $revision, 0 ); 01434 01435 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 01436 01437 # Update recentchanges 01438 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01439 global $wgUseRCPatrol, $wgUseNPPatrol; 01440 01441 # Mark as patrolled if the user can do so 01442 $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( 01443 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01444 # Add RC row to the DB 01445 $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, 01446 '', strlen( $text ), $revisionId, $patrolled ); 01447 01448 # Log auto-patrolled edits 01449 if ( $patrolled ) { 01450 PatrolLog::record( $rc, true ); 01451 } 01452 } 01453 $user->incEditCount(); 01454 $dbw->commit(); 01455 01456 # Update links, etc. 01457 $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); 01458 01459 wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary, 01460 $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); 01461 } 01462 01463 # Do updates right now unless deferral was requested 01464 if ( !( $flags & EDIT_DEFER_UPDATES ) ) { 01465 DeferredUpdates::doUpdates(); 01466 } 01467 01468 // Return the new revision (or null) to the caller 01469 $status->value['revision'] = $revision; 01470 01471 wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, 01472 $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); 01473 01474 # Promote user to any groups they meet the criteria for 01475 $user->addAutopromoteOnceGroups( 'onEdit' ); 01476 01477 wfProfileOut( __METHOD__ ); 01478 return $status; 01479 } 01480 01486 public function makeParserOptions( $user ) { 01487 global $wgContLang; 01488 if ( $user instanceof User ) { // settings per user (even anons) 01489 $options = ParserOptions::newFromUser( $user ); 01490 } else { // canonical settings 01491 $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); 01492 } 01493 $options->enableLimitReport(); // show inclusion/loop reports 01494 $options->setTidy( true ); // fix bad HTML 01495 return $options; 01496 } 01497 01502 public function prepareTextForEdit( $text, $revid = null, User $user = null ) { 01503 global $wgParser, $wgContLang, $wgUser; 01504 $user = is_null( $user ) ? $wgUser : $user; 01505 // @TODO fixme: check $user->getId() here??? 01506 if ( $this->mPreparedEdit 01507 && $this->mPreparedEdit->newText == $text 01508 && $this->mPreparedEdit->revid == $revid 01509 ) { 01510 // Already prepared 01511 return $this->mPreparedEdit; 01512 } 01513 01514 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); 01515 wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) ); 01516 01517 $edit = (object)array(); 01518 $edit->revid = $revid; 01519 $edit->newText = $text; 01520 $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); 01521 $edit->popts = $this->makeParserOptions( 'canonical' ); 01522 $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid ); 01523 $edit->oldText = $this->getRawText(); 01524 01525 $this->mPreparedEdit = $edit; 01526 01527 return $edit; 01528 } 01529 01547 public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { 01548 global $wgEnableParserCache; 01549 01550 wfProfileIn( __METHOD__ ); 01551 01552 $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); 01553 $text = $revision->getText(); 01554 01555 # Parse the text 01556 # Be careful not to double-PST: $text is usually already PST-ed once 01557 if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { 01558 wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); 01559 $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user ); 01560 } else { 01561 wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); 01562 $editInfo = $this->mPreparedEdit; 01563 } 01564 01565 # Save it to the parser cache 01566 if ( $wgEnableParserCache ) { 01567 $parserCache = ParserCache::singleton(); 01568 $parserCache->save( $editInfo->output, $this, $editInfo->popts ); 01569 } 01570 01571 # Update the links tables 01572 $u = new LinksUpdate( $this->mTitle, $editInfo->output ); 01573 $u->doUpdate(); 01574 01575 wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); 01576 01577 if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { 01578 if ( 0 == mt_rand( 0, 99 ) ) { 01579 // Flush old entries from the `recentchanges` table; we do this on 01580 // random requests so as to avoid an increase in writes for no good reason 01581 global $wgRCMaxAge; 01582 01583 $dbw = wfGetDB( DB_MASTER ); 01584 $cutoff = $dbw->timestamp( time() - $wgRCMaxAge ); 01585 $dbw->delete( 01586 'recentchanges', 01587 array( "rc_timestamp < '$cutoff'" ), 01588 __METHOD__ 01589 ); 01590 } 01591 } 01592 01593 if ( !$this->mTitle->exists() ) { 01594 wfProfileOut( __METHOD__ ); 01595 return; 01596 } 01597 01598 $id = $this->getId(); 01599 $title = $this->mTitle->getPrefixedDBkey(); 01600 $shortTitle = $this->mTitle->getDBkey(); 01601 01602 if ( !$options['changed'] ) { 01603 $good = 0; 01604 $total = 0; 01605 } elseif ( $options['created'] ) { 01606 $good = (int)$this->isCountable( $editInfo ); 01607 $total = 1; 01608 } elseif ( $options['oldcountable'] !== null ) { 01609 $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; 01610 $total = 0; 01611 } else { 01612 $good = 0; 01613 $total = 0; 01614 } 01615 01616 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) ); 01617 DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) ); 01618 01619 # If this is another user's talk page, update newtalk. 01620 # Don't do this if $options['changed'] = false (null-edits) nor if 01621 # it's a minor edit and the user doesn't want notifications for those. 01622 if ( $options['changed'] 01623 && $this->mTitle->getNamespace() == NS_USER_TALK 01624 && $shortTitle != $user->getTitleKey() 01625 && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) 01626 ) { 01627 if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) { 01628 $other = User::newFromName( $shortTitle, false ); 01629 if ( !$other ) { 01630 wfDebug( __METHOD__ . ": invalid username\n" ); 01631 } elseif ( User::isIP( $shortTitle ) ) { 01632 // An anonymous user 01633 $other->setNewtalk( true ); 01634 } elseif ( $other->isLoggedIn() ) { 01635 $other->setNewtalk( true ); 01636 } else { 01637 wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); 01638 } 01639 } 01640 } 01641 01642 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 01643 MessageCache::singleton()->replace( $shortTitle, $text ); 01644 } 01645 01646 if( $options['created'] ) { 01647 self::onArticleCreate( $this->mTitle ); 01648 } else { 01649 self::onArticleEdit( $this->mTitle ); 01650 } 01651 01652 wfProfileOut( __METHOD__ ); 01653 } 01654 01665 public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { 01666 wfProfileIn( __METHOD__ ); 01667 01668 $dbw = wfGetDB( DB_MASTER ); 01669 $revision = new Revision( array( 01670 'page' => $this->getId(), 01671 'text' => $text, 01672 'comment' => $comment, 01673 'minor_edit' => $minor ? 1 : 0, 01674 ) ); 01675 $revision->insertOn( $dbw ); 01676 $this->updateRevisionOn( $dbw, $revision ); 01677 01678 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 01679 01680 wfProfileOut( __METHOD__ ); 01681 } 01682 01694 public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) { 01695 global $wgContLang; 01696 01697 if ( wfReadOnly() ) { 01698 return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); 01699 } 01700 01701 $restrictionTypes = $this->mTitle->getRestrictionTypes(); 01702 01703 $id = $this->mTitle->getArticleID(); 01704 01705 if ( !$cascade ) { 01706 $cascade = false; 01707 } 01708 01709 // Take this opportunity to purge out expired restrictions 01710 Title::purgeExpiredRestrictions(); 01711 01712 # @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); 01713 # we expect a single selection, but the schema allows otherwise. 01714 $isProtected = false; 01715 $protect = false; 01716 $changed = false; 01717 01718 $dbw = wfGetDB( DB_MASTER ); 01719 01720 foreach ( $restrictionTypes as $action ) { 01721 if ( !isset( $expiry[$action] ) ) { 01722 $expiry[$action] = $dbw->getInfinity(); 01723 } 01724 if ( !isset( $limit[$action] ) ) { 01725 $limit[$action] = ''; 01726 } elseif ( $limit[$action] != '' ) { 01727 $protect = true; 01728 } 01729 01730 # Get current restrictions on $action 01731 $current = implode( '', $this->mTitle->getRestrictions( $action ) ); 01732 if ( $current != '' ) { 01733 $isProtected = true; 01734 } 01735 01736 if ( $limit[$action] != $current ) { 01737 $changed = true; 01738 } elseif ( $limit[$action] != '' ) { 01739 # Only check expiry change if the action is actually being 01740 # protected, since expiry does nothing on an not-protected 01741 # action. 01742 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { 01743 $changed = true; 01744 } 01745 } 01746 } 01747 01748 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) { 01749 $changed = true; 01750 } 01751 01752 # If nothing's changed, do nothing 01753 if ( !$changed ) { 01754 return Status::newGood(); 01755 } 01756 01757 if ( !$protect ) { # No protection at all means unprotection 01758 $revCommentMsg = 'unprotectedarticle'; 01759 $logAction = 'unprotect'; 01760 } elseif ( $isProtected ) { 01761 $revCommentMsg = 'modifiedarticleprotection'; 01762 $logAction = 'modify'; 01763 } else { 01764 $revCommentMsg = 'protectedarticle'; 01765 $logAction = 'protect'; 01766 } 01767 01768 $encodedExpiry = array(); 01769 $protectDescription = ''; 01770 foreach ( $limit as $action => $restrictions ) { 01771 $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] ); 01772 if ( $restrictions != '' ) { 01773 $protectDescription .= $wgContLang->getDirMark() . "[$action=$restrictions] ("; 01774 if ( $encodedExpiry[$action] != 'infinity' ) { 01775 $protectDescription .= wfMsgForContent( 'protect-expiring', 01776 $wgContLang->timeanddate( $expiry[$action], false, false ) , 01777 $wgContLang->date( $expiry[$action], false, false ) , 01778 $wgContLang->time( $expiry[$action], false, false ) ); 01779 } else { 01780 $protectDescription .= wfMsgForContent( 'protect-expiry-indefinite' ); 01781 } 01782 01783 $protectDescription .= ') '; 01784 } 01785 } 01786 $protectDescription = trim( $protectDescription ); 01787 01788 if ( $id ) { # Protection of existing page 01789 if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { 01790 return Status::newGood(); 01791 } 01792 01793 # Only restrictions with the 'protect' right can cascade... 01794 # Otherwise, people who cannot normally protect can "protect" pages via transclusion 01795 $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' ); 01796 01797 # The schema allows multiple restrictions 01798 if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) ) { 01799 $cascade = false; 01800 } 01801 01802 # Update restrictions table 01803 foreach ( $limit as $action => $restrictions ) { 01804 if ( $restrictions != '' ) { 01805 $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ), 01806 array( 'pr_page' => $id, 01807 'pr_type' => $action, 01808 'pr_level' => $restrictions, 01809 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, 01810 'pr_expiry' => $encodedExpiry[$action] 01811 ), 01812 __METHOD__ 01813 ); 01814 } else { 01815 $dbw->delete( 'page_restrictions', array( 'pr_page' => $id, 01816 'pr_type' => $action ), __METHOD__ ); 01817 } 01818 } 01819 01820 # Prepare a null revision to be added to the history 01821 $editComment = $wgContLang->ucfirst( wfMsgForContent( $revCommentMsg, $this->mTitle->getPrefixedText() ) ); 01822 if ( $reason ) { 01823 $editComment .= ": $reason"; 01824 } 01825 if ( $protectDescription ) { 01826 $editComment .= " ($protectDescription)"; 01827 } 01828 if ( $cascade ) { 01829 $editComment .= ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']'; 01830 } 01831 01832 # Insert a null revision 01833 $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true ); 01834 $nullRevId = $nullRevision->insertOn( $dbw ); 01835 01836 $latest = $this->getLatest(); 01837 # Update page record 01838 $dbw->update( 'page', 01839 array( /* SET */ 01840 'page_touched' => $dbw->timestamp(), 01841 'page_restrictions' => '', 01842 'page_latest' => $nullRevId 01843 ), array( /* WHERE */ 01844 'page_id' => $id 01845 ), __METHOD__ 01846 ); 01847 01848 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); 01849 wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); 01850 } else { # Protection of non-existing page (also known as "title protection") 01851 # Cascade protection is meaningless in this case 01852 $cascade = false; 01853 01854 if ( $limit['create'] != '' ) { 01855 $dbw->replace( 'protected_titles', 01856 array( array( 'pt_namespace', 'pt_title' ) ), 01857 array( 01858 'pt_namespace' => $this->mTitle->getNamespace(), 01859 'pt_title' => $this->mTitle->getDBkey(), 01860 'pt_create_perm' => $limit['create'], 01861 'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ), 01862 'pt_expiry' => $encodedExpiry['create'], 01863 'pt_user' => $user->getId(), 01864 'pt_reason' => $reason, 01865 ), __METHOD__ 01866 ); 01867 } else { 01868 $dbw->delete( 'protected_titles', 01869 array( 01870 'pt_namespace' => $this->mTitle->getNamespace(), 01871 'pt_title' => $this->mTitle->getDBkey() 01872 ), __METHOD__ 01873 ); 01874 } 01875 } 01876 01877 $this->mTitle->flushRestrictions(); 01878 01879 if ( $logAction == 'unprotect' ) { 01880 $logParams = array(); 01881 } else { 01882 $logParams = array( $protectDescription, $cascade ? 'cascade' : '' ); 01883 } 01884 01885 # Update the protection log 01886 $log = new LogPage( 'protect' ); 01887 $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user ); 01888 01889 return Status::newGood(); 01890 } 01891 01898 protected static function flattenRestrictions( $limit ) { 01899 if ( !is_array( $limit ) ) { 01900 throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' ); 01901 } 01902 01903 $bits = array(); 01904 ksort( $limit ); 01905 01906 foreach ( $limit as $action => $restrictions ) { 01907 if ( $restrictions != '' ) { 01908 $bits[] = "$action=$restrictions"; 01909 } 01910 } 01911 01912 return implode( ':', $bits ); 01913 } 01914 01931 public function doDeleteArticle( 01932 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 01933 ) { 01934 return $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ) 01935 == WikiPage::DELETE_SUCCESS; 01936 } 01937 01954 public function doDeleteArticleReal( 01955 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 01956 ) { 01957 global $wgUser; 01958 $user = is_null( $user ) ? $wgUser : $user; 01959 01960 wfDebug( __METHOD__ . "\n" ); 01961 01962 if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error ) ) ) { 01963 return WikiPage::DELETE_HOOK_ABORTED; 01964 } 01965 $dbw = wfGetDB( DB_MASTER ); 01966 $t = $this->mTitle->getDBkey(); 01967 $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); 01968 01969 if ( $t === '' || $id == 0 ) { 01970 return WikiPage::DELETE_NO_PAGE; 01971 } 01972 01973 // Bitfields to further suppress the content 01974 if ( $suppress ) { 01975 $bitfield = 0; 01976 // This should be 15... 01977 $bitfield |= Revision::DELETED_TEXT; 01978 $bitfield |= Revision::DELETED_COMMENT; 01979 $bitfield |= Revision::DELETED_USER; 01980 $bitfield |= Revision::DELETED_RESTRICTED; 01981 } else { 01982 $bitfield = 'rev_deleted'; 01983 } 01984 01985 $dbw->begin(); 01986 // For now, shunt the revision data into the archive table. 01987 // Text is *not* removed from the text table; bulk storage 01988 // is left intact to avoid breaking block-compression or 01989 // immutable storage schemes. 01990 // 01991 // For backwards compatibility, note that some older archive 01992 // table entries will have ar_text and ar_flags fields still. 01993 // 01994 // In the future, we may keep revisions and mark them with 01995 // the rev_deleted field, which is reserved for this purpose. 01996 $dbw->insertSelect( 'archive', array( 'page', 'revision' ), 01997 array( 01998 'ar_namespace' => 'page_namespace', 01999 'ar_title' => 'page_title', 02000 'ar_comment' => 'rev_comment', 02001 'ar_user' => 'rev_user', 02002 'ar_user_text' => 'rev_user_text', 02003 'ar_timestamp' => 'rev_timestamp', 02004 'ar_minor_edit' => 'rev_minor_edit', 02005 'ar_rev_id' => 'rev_id', 02006 'ar_parent_id' => 'rev_parent_id', 02007 'ar_text_id' => 'rev_text_id', 02008 'ar_text' => '\'\'', // Be explicit to appease 02009 'ar_flags' => '\'\'', // MySQL's "strict mode"... 02010 'ar_len' => 'rev_len', 02011 'ar_page_id' => 'page_id', 02012 'ar_deleted' => $bitfield, 02013 'ar_sha1' => 'rev_sha1' 02014 ), array( 02015 'page_id' => $id, 02016 'page_id = rev_page' 02017 ), __METHOD__ 02018 ); 02019 02020 # Now that it's safely backed up, delete it 02021 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); 02022 $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy 02023 02024 if ( !$ok ) { 02025 $dbw->rollback(); 02026 return WikiPage::DELETE_NO_REVISIONS; 02027 } 02028 02029 $this->doDeleteUpdates( $id ); 02030 02031 # Log the deletion, if the page was suppressed, log it at Oversight instead 02032 $logtype = $suppress ? 'suppress' : 'delete'; 02033 02034 $logEntry = new ManualLogEntry( $logtype, 'delete' ); 02035 $logEntry->setPerformer( $user ); 02036 $logEntry->setTarget( $this->mTitle ); 02037 $logEntry->setComment( $reason ); 02038 $logid = $logEntry->insert(); 02039 $logEntry->publish( $logid ); 02040 02041 if ( $commit ) { 02042 $dbw->commit(); 02043 } 02044 02045 wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) ); 02046 return WikiPage::DELETE_SUCCESS; 02047 } 02048 02054 public function doDeleteUpdates( $id ) { 02055 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); 02056 02057 $dbw = wfGetDB( DB_MASTER ); 02058 02059 # Delete restrictions for it 02060 $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); 02061 02062 # Fix category table counts 02063 $cats = array(); 02064 $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ ); 02065 02066 foreach ( $res as $row ) { 02067 $cats [] = $row->cl_to; 02068 } 02069 02070 $this->updateCategoryCounts( array(), $cats ); 02071 02072 # If using cascading deletes, we can skip some explicit deletes 02073 if ( !$dbw->cascadingDeletes() ) { 02074 $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); 02075 02076 # Delete outgoing links 02077 $dbw->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ ); 02078 $dbw->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ ); 02079 $dbw->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ ); 02080 $dbw->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ ); 02081 $dbw->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ ); 02082 $dbw->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ ); 02083 $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ ); 02084 $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ ); 02085 $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ ); 02086 } 02087 02088 # If using cleanup triggers, we can skip some manual deletes 02089 if ( !$dbw->cleanupTriggers() ) { 02090 # Clean up recentchanges entries... 02091 $dbw->delete( 'recentchanges', 02092 array( 'rc_type != ' . RC_LOG, 02093 'rc_namespace' => $this->mTitle->getNamespace(), 02094 'rc_title' => $this->mTitle->getDBkey() ), 02095 __METHOD__ ); 02096 $dbw->delete( 'recentchanges', 02097 array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ), 02098 __METHOD__ ); 02099 } 02100 02101 # Clear caches 02102 self::onArticleDelete( $this->mTitle ); 02103 02104 # Clear the cached article id so the interface doesn't act like we exist 02105 $this->mTitle->resetArticleID( 0 ); 02106 } 02107 02132 public function doRollback( 02133 $fromP, $summary, $token, $bot, &$resultDetails, User $user 02134 ) { 02135 $resultDetails = null; 02136 02137 # Check permissions 02138 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); 02139 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); 02140 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); 02141 02142 if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { 02143 $errors[] = array( 'sessionfailure' ); 02144 } 02145 02146 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) { 02147 $errors[] = array( 'actionthrottledtext' ); 02148 } 02149 02150 # If there were errors, bail out now 02151 if ( !empty( $errors ) ) { 02152 return $errors; 02153 } 02154 02155 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user ); 02156 } 02157 02173 public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { 02174 global $wgUseRCPatrol, $wgContLang; 02175 02176 $dbw = wfGetDB( DB_MASTER ); 02177 02178 if ( wfReadOnly() ) { 02179 return array( array( 'readonlytext' ) ); 02180 } 02181 02182 # Get the last editor 02183 $current = $this->getRevision(); 02184 if ( is_null( $current ) ) { 02185 # Something wrong... no page? 02186 return array( array( 'notanarticle' ) ); 02187 } 02188 02189 $from = str_replace( '_', ' ', $fromP ); 02190 # User name given should match up with the top revision. 02191 # If the user was deleted then $from should be empty. 02192 if ( $from != $current->getUserText() ) { 02193 $resultDetails = array( 'current' => $current ); 02194 return array( array( 'alreadyrolled', 02195 htmlspecialchars( $this->mTitle->getPrefixedText() ), 02196 htmlspecialchars( $fromP ), 02197 htmlspecialchars( $current->getUserText() ) 02198 ) ); 02199 } 02200 02201 # Get the last edit not by this guy... 02202 # Note: these may not be public values 02203 $user = intval( $current->getRawUser() ); 02204 $user_text = $dbw->addQuotes( $current->getRawUserText() ); 02205 $s = $dbw->selectRow( 'revision', 02206 array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), 02207 array( 'rev_page' => $current->getPage(), 02208 "rev_user != {$user} OR rev_user_text != {$user_text}" 02209 ), __METHOD__, 02210 array( 'USE INDEX' => 'page_timestamp', 02211 'ORDER BY' => 'rev_timestamp DESC' ) 02212 ); 02213 if ( $s === false ) { 02214 # No one else ever edited this page 02215 return array( array( 'cantrollback' ) ); 02216 } elseif ( $s->rev_deleted & Revision::DELETED_TEXT || $s->rev_deleted & Revision::DELETED_USER ) { 02217 # Only admins can see this text 02218 return array( array( 'notvisiblerev' ) ); 02219 } 02220 02221 $set = array(); 02222 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { 02223 # Mark all reverted edits as bot 02224 $set['rc_bot'] = 1; 02225 } 02226 02227 if ( $wgUseRCPatrol ) { 02228 # Mark all reverted edits as patrolled 02229 $set['rc_patrolled'] = 1; 02230 } 02231 02232 if ( count( $set ) ) { 02233 $dbw->update( 'recentchanges', $set, 02234 array( /* WHERE */ 02235 'rc_cur_id' => $current->getPage(), 02236 'rc_user_text' => $current->getUserText(), 02237 "rc_timestamp > '{$s->rev_timestamp}'", 02238 ), __METHOD__ 02239 ); 02240 } 02241 02242 # Generate the edit summary if necessary 02243 $target = Revision::newFromId( $s->rev_id ); 02244 if ( empty( $summary ) ) { 02245 if ( $from == '' ) { // no public user name 02246 $summary = wfMsgForContent( 'revertpage-nouser' ); 02247 } else { 02248 $summary = wfMsgForContent( 'revertpage' ); 02249 } 02250 } 02251 02252 # Allow the custom summary to use the same args as the default message 02253 $args = array( 02254 $target->getUserText(), $from, $s->rev_id, 02255 $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), 02256 $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) 02257 ); 02258 $summary = wfMsgReplaceArgs( $summary, $args ); 02259 02260 # Save 02261 $flags = EDIT_UPDATE; 02262 02263 if ( $guser->isAllowed( 'minoredit' ) ) { 02264 $flags |= EDIT_MINOR; 02265 } 02266 02267 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { 02268 $flags |= EDIT_FORCE_BOT; 02269 } 02270 02271 # Actually store the edit 02272 $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser ); 02273 if ( !empty( $status->value['revision'] ) ) { 02274 $revId = $status->value['revision']->getId(); 02275 } else { 02276 $revId = false; 02277 } 02278 02279 wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) ); 02280 02281 $resultDetails = array( 02282 'summary' => $summary, 02283 'current' => $current, 02284 'target' => $target, 02285 'newid' => $revId 02286 ); 02287 02288 return array(); 02289 } 02290 02302 public static function onArticleCreate( $title ) { 02303 # Update existence markers on article/talk tabs... 02304 if ( $title->isTalkPage() ) { 02305 $other = $title->getSubjectPage(); 02306 } else { 02307 $other = $title->getTalkPage(); 02308 } 02309 02310 $other->invalidateCache(); 02311 $other->purgeSquid(); 02312 02313 $title->touchLinks(); 02314 $title->purgeSquid(); 02315 $title->deleteTitleProtection(); 02316 } 02317 02323 public static function onArticleDelete( $title ) { 02324 # Update existence markers on article/talk tabs... 02325 if ( $title->isTalkPage() ) { 02326 $other = $title->getSubjectPage(); 02327 } else { 02328 $other = $title->getTalkPage(); 02329 } 02330 02331 $other->invalidateCache(); 02332 $other->purgeSquid(); 02333 02334 $title->touchLinks(); 02335 $title->purgeSquid(); 02336 02337 # File cache 02338 HTMLFileCache::clearFileCache( $title ); 02339 02340 # Messages 02341 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 02342 MessageCache::singleton()->replace( $title->getDBkey(), false ); 02343 } 02344 02345 # Images 02346 if ( $title->getNamespace() == NS_FILE ) { 02347 $update = new HTMLCacheUpdate( $title, 'imagelinks' ); 02348 $update->doUpdate(); 02349 } 02350 02351 # User talk pages 02352 if ( $title->getNamespace() == NS_USER_TALK ) { 02353 $user = User::newFromName( $title->getText(), false ); 02354 if ( $user ) { 02355 $user->setNewtalk( false ); 02356 } 02357 } 02358 02359 # Image redirects 02360 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); 02361 } 02362 02369 public static function onArticleEdit( $title ) { 02370 // Invalidate caches of articles which include this page 02371 DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); 02372 02373 02374 // Invalidate the caches of all pages which redirect here 02375 DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); 02376 02377 # Purge squid for this page only 02378 $title->purgeSquid(); 02379 02380 # Clear file cache for this page only 02381 HTMLFileCache::clearFileCache( $title ); 02382 } 02383 02392 public function getHiddenCategories() { 02393 $result = array(); 02394 $id = $this->mTitle->getArticleID(); 02395 02396 if ( $id == 0 ) { 02397 return array(); 02398 } 02399 02400 $dbr = wfGetDB( DB_SLAVE ); 02401 $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), 02402 array( 'cl_to' ), 02403 array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', 02404 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), 02405 __METHOD__ ); 02406 02407 if ( $res !== false ) { 02408 foreach ( $res as $row ) { 02409 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); 02410 } 02411 } 02412 02413 return $result; 02414 } 02415 02423 public static function getAutosummary( $oldtext, $newtext, $flags ) { 02424 global $wgContLang; 02425 02426 # Decide what kind of autosummary is needed. 02427 02428 # Redirect autosummaries 02429 $ot = Title::newFromRedirect( $oldtext ); 02430 $rt = Title::newFromRedirect( $newtext ); 02431 02432 if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) { 02433 $truncatedtext = $wgContLang->truncate( 02434 str_replace( "\n", ' ', $newtext ), 02435 max( 0, 250 02436 - strlen( wfMsgForContent( 'autoredircomment' ) ) 02437 - strlen( $rt->getFullText() ) 02438 ) ); 02439 return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext ); 02440 } 02441 02442 # New page autosummaries 02443 if ( $flags & EDIT_NEW && strlen( $newtext ) ) { 02444 # If they're making a new article, give its text, truncated, in the summary. 02445 02446 $truncatedtext = $wgContLang->truncate( 02447 str_replace( "\n", ' ', $newtext ), 02448 max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) ); 02449 02450 return wfMsgForContent( 'autosumm-new', $truncatedtext ); 02451 } 02452 02453 # Blanking autosummaries 02454 if ( $oldtext != '' && $newtext == '' ) { 02455 return wfMsgForContent( 'autosumm-blank' ); 02456 } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) { 02457 # Removing more than 90% of the article 02458 02459 $truncatedtext = $wgContLang->truncate( 02460 $newtext, 02461 max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) ); 02462 02463 return wfMsgForContent( 'autosumm-replace', $truncatedtext ); 02464 } 02465 02466 # If we reach this point, there's no applicable autosummary for our case, so our 02467 # autosummary is empty. 02468 return ''; 02469 } 02470 02478 public function getAutoDeleteReason( &$hasHistory ) { 02479 global $wgContLang; 02480 02481 // Get the last revision 02482 $rev = $this->getRevision(); 02483 02484 if ( is_null( $rev ) ) { 02485 return false; 02486 } 02487 02488 // Get the article's contents 02489 $contents = $rev->getText(); 02490 $blank = false; 02491 02492 // If the page is blank, use the text from the previous revision, 02493 // which can only be blank if there's a move/import/protect dummy revision involved 02494 if ( $contents == '' ) { 02495 $prev = $rev->getPrevious(); 02496 02497 if ( $prev ) { 02498 $contents = $prev->getText(); 02499 $blank = true; 02500 } 02501 } 02502 02503 $dbw = wfGetDB( DB_MASTER ); 02504 02505 // Find out if there was only one contributor 02506 // Only scan the last 20 revisions 02507 $res = $dbw->select( 'revision', 'rev_user_text', 02508 array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ), 02509 __METHOD__, 02510 array( 'LIMIT' => 20 ) 02511 ); 02512 02513 if ( $res === false ) { 02514 // This page has no revisions, which is very weird 02515 return false; 02516 } 02517 02518 $hasHistory = ( $res->numRows() > 1 ); 02519 $row = $dbw->fetchObject( $res ); 02520 02521 if ( $row ) { // $row is false if the only contributor is hidden 02522 $onlyAuthor = $row->rev_user_text; 02523 // Try to find a second contributor 02524 foreach ( $res as $row ) { 02525 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 02526 $onlyAuthor = false; 02527 break; 02528 } 02529 } 02530 } else { 02531 $onlyAuthor = false; 02532 } 02533 02534 // Generate the summary with a '$1' placeholder 02535 if ( $blank ) { 02536 // The current revision is blank and the one before is also 02537 // blank. It's just not our lucky day 02538 $reason = wfMsgForContent( 'exbeforeblank', '$1' ); 02539 } else { 02540 if ( $onlyAuthor ) { 02541 $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor ); 02542 } else { 02543 $reason = wfMsgForContent( 'excontent', '$1' ); 02544 } 02545 } 02546 02547 if ( $reason == '-' ) { 02548 // Allow these UI messages to be blanked out cleanly 02549 return ''; 02550 } 02551 02552 // Replace newlines with spaces to prevent uglyness 02553 $contents = preg_replace( "/[\n\r]/", ' ', $contents ); 02554 // Calculate the maximum amount of chars to get 02555 // Max content length = max comment length - length of the comment (excl. $1) 02556 $maxLength = 255 - ( strlen( $reason ) - 2 ); 02557 $contents = $wgContLang->truncate( $contents, $maxLength ); 02558 // Remove possible unfinished links 02559 $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); 02560 // Now replace the '$1' placeholder 02561 $reason = str_replace( '$1', $contents, $reason ); 02562 02563 return $reason; 02564 } 02565 02573 public function updateCategoryCounts( $added, $deleted ) { 02574 $ns = $this->mTitle->getNamespace(); 02575 $dbw = wfGetDB( DB_MASTER ); 02576 02577 # First make sure the rows exist. If one of the "deleted" ones didn't 02578 # exist, we might legitimately not create it, but it's simpler to just 02579 # create it and then give it a negative value, since the value is bogus 02580 # anyway. 02581 # 02582 # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE. 02583 $insertCats = array_merge( $added, $deleted ); 02584 if ( !$insertCats ) { 02585 # Okay, nothing to do 02586 return; 02587 } 02588 02589 $insertRows = array(); 02590 02591 foreach ( $insertCats as $cat ) { 02592 $insertRows[] = array( 02593 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ), 02594 'cat_title' => $cat 02595 ); 02596 } 02597 $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' ); 02598 02599 $addFields = array( 'cat_pages = cat_pages + 1' ); 02600 $removeFields = array( 'cat_pages = cat_pages - 1' ); 02601 02602 if ( $ns == NS_CATEGORY ) { 02603 $addFields[] = 'cat_subcats = cat_subcats + 1'; 02604 $removeFields[] = 'cat_subcats = cat_subcats - 1'; 02605 } elseif ( $ns == NS_FILE ) { 02606 $addFields[] = 'cat_files = cat_files + 1'; 02607 $removeFields[] = 'cat_files = cat_files - 1'; 02608 } 02609 02610 if ( $added ) { 02611 $dbw->update( 02612 'category', 02613 $addFields, 02614 array( 'cat_title' => $added ), 02615 __METHOD__ 02616 ); 02617 } 02618 02619 if ( $deleted ) { 02620 $dbw->update( 02621 'category', 02622 $removeFields, 02623 array( 'cat_title' => $deleted ), 02624 __METHOD__ 02625 ); 02626 } 02627 } 02628 02634 public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) { 02635 if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { 02636 return; 02637 } 02638 02639 // templatelinks table may have become out of sync, 02640 // especially if using variable-based transclusions. 02641 // For paranoia, check if things have changed and if 02642 // so apply updates to the database. This will ensure 02643 // that cascaded protections apply as soon as the changes 02644 // are visible. 02645 02646 # Get templates from templatelinks 02647 $id = $this->mTitle->getArticleID(); 02648 02649 $tlTemplates = array(); 02650 02651 $dbr = wfGetDB( DB_SLAVE ); 02652 $res = $dbr->select( array( 'templatelinks' ), 02653 array( 'tl_namespace', 'tl_title' ), 02654 array( 'tl_from' => $id ), 02655 __METHOD__ 02656 ); 02657 02658 foreach ( $res as $row ) { 02659 $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; 02660 } 02661 02662 # Get templates from parser output. 02663 $poTemplates = array(); 02664 foreach ( $parserOutput->getTemplates() as $ns => $templates ) { 02665 foreach ( $templates as $dbk => $id ) { 02666 $poTemplates["$ns:$dbk"] = true; 02667 } 02668 } 02669 02670 # Get the diff 02671 $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); 02672 02673 if ( count( $templates_diff ) > 0 ) { 02674 # Whee, link updates time. 02675 $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); 02676 $u->doUpdate(); 02677 } 02678 } 02679 02687 public function getUsedTemplates() { 02688 return $this->mTitle->getTemplateLinksFrom(); 02689 } 02690 02701 public function createUpdates( $rev ) { 02702 wfDeprecated( __METHOD__, '1.18' ); 02703 global $wgUser; 02704 $this->doEditUpdates( $rev, $wgUser, array( 'created' => true ) ); 02705 } 02706 02719 public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) { 02720 global $wgParser, $wgUser; 02721 02722 wfDeprecated( __METHOD__, '1.19' ); 02723 02724 $user = is_null( $user ) ? $wgUser : $user; 02725 02726 if ( $popts === null ) { 02727 $popts = ParserOptions::newFromUser( $user ); 02728 } 02729 02730 return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); 02731 } 02732 02739 public function isBigDeletion() { 02740 wfDeprecated( __METHOD__, '1.19' ); 02741 return $this->mTitle->isBigDeletion(); 02742 } 02743 02750 public function estimateRevisionCount() { 02751 wfDeprecated( __METHOD__, '1.19' ); 02752 return $this->mTitle->estimateRevisionCount(); 02753 } 02754 02766 public function updateRestrictions( 02767 $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null 02768 ) { 02769 global $wgUser; 02770 02771 $user = is_null( $user ) ? $wgUser : $user; 02772 02773 return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK(); 02774 } 02775 02779 public function quickEdit( $text, $comment = '', $minor = 0 ) { 02780 wfDeprecated( __METHOD__, '1.18' ); 02781 global $wgUser; 02782 return $this->doQuickEdit( $text, $wgUser, $comment, $minor ); 02783 } 02784 02788 public function viewUpdates() { 02789 wfDeprecated( __METHOD__, '1.18' ); 02790 global $wgUser; 02791 return $this->doViewUpdates( $wgUser ); 02792 } 02793 02797 public function useParserCache( $oldid ) { 02798 wfDeprecated( __METHOD__, '1.18' ); 02799 global $wgUser; 02800 return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid ); 02801 } 02802 } 02803 02804 class PoolWorkArticleView extends PoolCounterWork { 02805 02809 private $page; 02810 02814 private $cacheKey; 02815 02819 private $revid; 02820 02824 private $parserOptions; 02825 02829 private $text; 02830 02834 private $parserOutput = false; 02835 02839 private $isDirty = false; 02840 02844 private $error = false; 02845 02855 function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) { 02856 $this->page = $page; 02857 $this->revid = $revid; 02858 $this->cacheable = $useParserCache; 02859 $this->parserOptions = $parserOptions; 02860 $this->text = $text; 02861 $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions ); 02862 parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid ); 02863 } 02864 02870 public function getParserOutput() { 02871 return $this->parserOutput; 02872 } 02873 02879 public function getIsDirty() { 02880 return $this->isDirty; 02881 } 02882 02888 public function getError() { 02889 return $this->error; 02890 } 02891 02895 function doWork() { 02896 global $wgParser, $wgUseFileCache; 02897 02898 $isCurrent = $this->revid === $this->page->getLatest(); 02899 02900 if ( $this->text !== null ) { 02901 $text = $this->text; 02902 } elseif ( $isCurrent ) { 02903 $text = $this->page->getRawText(); 02904 } else { 02905 $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid ); 02906 if ( $rev === null ) { 02907 return false; 02908 } 02909 $text = $rev->getText(); 02910 } 02911 02912 $time = - microtime( true ); 02913 $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(), 02914 $this->parserOptions, true, true, $this->revid ); 02915 $time += microtime( true ); 02916 02917 # Timing hack 02918 if ( $time > 3 ) { 02919 wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, 02920 $this->page->getTitle()->getPrefixedDBkey() ) ); 02921 } 02922 02923 if ( $this->cacheable && $this->parserOutput->isCacheable() ) { 02924 ParserCache::singleton()->save( $this->parserOutput, $this->page, $this->parserOptions ); 02925 } 02926 02927 // Make sure file cache is not used on uncacheable content. 02928 // Output that has magic words in it can still use the parser cache 02929 // (if enabled), though it will generally expire sooner. 02930 if ( !$this->parserOutput->isCacheable() || $this->parserOutput->containsOldMagic() ) { 02931 $wgUseFileCache = false; 02932 } 02933 02934 if ( $isCurrent ) { 02935 $this->page->doCascadeProtectionUpdates( $this->parserOutput ); 02936 } 02937 02938 return true; 02939 } 02940 02944 function getCachedWork() { 02945 $this->parserOutput = ParserCache::singleton()->get( $this->page, $this->parserOptions ); 02946 02947 if ( $this->parserOutput === false ) { 02948 wfDebug( __METHOD__ . ": parser cache miss\n" ); 02949 return false; 02950 } else { 02951 wfDebug( __METHOD__ . ": parser cache hit\n" ); 02952 return true; 02953 } 02954 } 02955 02959 function fallback() { 02960 $this->parserOutput = ParserCache::singleton()->getDirty( $this->page, $this->parserOptions ); 02961 02962 if ( $this->parserOutput === false ) { 02963 wfDebugLog( 'dirty', "dirty missing\n" ); 02964 wfDebug( __METHOD__ . ": no dirty cache\n" ); 02965 return false; 02966 } else { 02967 wfDebug( __METHOD__ . ": sending dirty output\n" ); 02968 wfDebugLog( 'dirty', "dirty output {$this->cacheKey}\n" ); 02969 $this->isDirty = true; 02970 return true; 02971 } 02972 } 02973 02977 function error( $status ) { 02978 $this->error = $status; 02979 return false; 02980 } 02981 }