MediaWiki
REL1_24
|
00001 <?php 00026 interface Page { 00027 } 00028 00037 class WikiPage implements Page, IDBAccessObject { 00038 // Constants for $mDataLoadedFrom and related 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") 00054 public $mPreparedEdit = false; 00055 00059 protected $mId = null; 00060 00064 protected $mDataLoadedFrom = self::READ_NONE; 00065 00069 protected $mRedirectTarget = null; 00070 00074 protected $mLastRevision = null; 00075 00079 protected $mTimestamp = ''; 00080 00084 protected $mTouched = '19700101000000'; 00085 00089 protected $mLinksUpdated = '19700101000000'; 00090 00094 protected $mCounter = null; 00095 00100 public function __construct( Title $title ) { 00101 $this->mTitle = $title; 00102 } 00103 00112 public static function factory( Title $title ) { 00113 $ns = $title->getNamespace(); 00114 00115 if ( $ns == NS_MEDIA ) { 00116 throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." ); 00117 } elseif ( $ns < 0 ) { 00118 throw new MWException( "Invalid or virtual namespace $ns given." ); 00119 } 00120 00121 switch ( $ns ) { 00122 case NS_FILE: 00123 $page = new WikiFilePage( $title ); 00124 break; 00125 case NS_CATEGORY: 00126 $page = new WikiCategoryPage( $title ); 00127 break; 00128 default: 00129 $page = new WikiPage( $title ); 00130 } 00131 00132 return $page; 00133 } 00134 00145 public static function newFromID( $id, $from = 'fromdb' ) { 00146 // page id's are never 0 or negative, see bug 61166 00147 if ( $id < 1 ) { 00148 return null; 00149 } 00150 00151 $from = self::convertSelectType( $from ); 00152 $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE ); 00153 $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ ); 00154 if ( !$row ) { 00155 return null; 00156 } 00157 return self::newFromRow( $row, $from ); 00158 } 00159 00171 public static function newFromRow( $row, $from = 'fromdb' ) { 00172 $page = self::factory( Title::newFromRow( $row ) ); 00173 $page->loadFromRow( $row, $from ); 00174 return $page; 00175 } 00176 00183 private static function convertSelectType( $type ) { 00184 switch ( $type ) { 00185 case 'fromdb': 00186 return self::READ_NORMAL; 00187 case 'fromdbmaster': 00188 return self::READ_LATEST; 00189 case 'forupdate': 00190 return self::READ_LOCKING; 00191 default: 00192 // It may already be an integer or whatever else 00193 return $type; 00194 } 00195 } 00196 00207 public function getActionOverrides() { 00208 $content_handler = $this->getContentHandler(); 00209 return $content_handler->getActionOverrides(); 00210 } 00211 00221 public function getContentHandler() { 00222 return ContentHandler::getForModelID( $this->getContentModel() ); 00223 } 00224 00229 public function getTitle() { 00230 return $this->mTitle; 00231 } 00232 00237 public function clear() { 00238 $this->mDataLoaded = false; 00239 $this->mDataLoadedFrom = self::READ_NONE; 00240 00241 $this->clearCacheFields(); 00242 } 00243 00248 protected function clearCacheFields() { 00249 $this->mId = null; 00250 $this->mCounter = null; 00251 $this->mRedirectTarget = null; // Title object if set 00252 $this->mLastRevision = null; // Latest revision 00253 $this->mTouched = '19700101000000'; 00254 $this->mLinksUpdated = '19700101000000'; 00255 $this->mTimestamp = ''; 00256 $this->mIsRedirect = false; 00257 $this->mLatest = false; 00258 // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks 00259 // the requested rev ID and content against the cached one for equality. For most 00260 // content types, the output should not change during the lifetime of this cache. 00261 // Clearing it can cause extra parses on edit for no reason. 00262 } 00263 00269 public function clearPreparedEdit() { 00270 $this->mPreparedEdit = false; 00271 } 00272 00279 public static function selectFields() { 00280 global $wgContentHandlerUseDB, $wgPageLanguageUseDB; 00281 00282 $fields = array( 00283 'page_id', 00284 'page_namespace', 00285 'page_title', 00286 'page_restrictions', 00287 'page_counter', 00288 'page_is_redirect', 00289 'page_is_new', 00290 'page_random', 00291 'page_touched', 00292 'page_links_updated', 00293 'page_latest', 00294 'page_len', 00295 ); 00296 00297 if ( $wgContentHandlerUseDB ) { 00298 $fields[] = 'page_content_model'; 00299 } 00300 00301 if ( $wgPageLanguageUseDB ) { 00302 $fields[] = 'page_lang'; 00303 } 00304 00305 return $fields; 00306 } 00307 00315 protected function pageData( $dbr, $conditions, $options = array() ) { 00316 $fields = self::selectFields(); 00317 00318 wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); 00319 00320 $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); 00321 00322 wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); 00323 00324 return $row; 00325 } 00326 00336 public function pageDataFromTitle( $dbr, $title, $options = array() ) { 00337 return $this->pageData( $dbr, array( 00338 'page_namespace' => $title->getNamespace(), 00339 'page_title' => $title->getDBkey() ), $options ); 00340 } 00341 00350 public function pageDataFromId( $dbr, $id, $options = array() ) { 00351 return $this->pageData( $dbr, array( 'page_id' => $id ), $options ); 00352 } 00353 00367 public function loadPageData( $from = 'fromdb' ) { 00368 $from = self::convertSelectType( $from ); 00369 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) { 00370 // We already have the data from the correct location, no need to load it twice. 00371 return; 00372 } 00373 00374 if ( $from === self::READ_LOCKING ) { 00375 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); 00376 } elseif ( $from === self::READ_LATEST ) { 00377 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00378 } elseif ( $from === self::READ_NORMAL ) { 00379 $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); 00380 // Use a "last rev inserted" timestamp key to diminish the issue of slave lag. 00381 // Note that DB also stores the master position in the session and checks it. 00382 $touched = $this->getCachedLastEditTime(); 00383 if ( $touched ) { // key set 00384 if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) { 00385 $from = self::READ_LATEST; 00386 $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); 00387 } 00388 } 00389 } else { 00390 // No idea from where the caller got this data, assume slave database. 00391 $data = $from; 00392 $from = self::READ_NORMAL; 00393 } 00394 00395 $this->loadFromRow( $data, $from ); 00396 } 00397 00409 public function loadFromRow( $data, $from ) { 00410 $lc = LinkCache::singleton(); 00411 $lc->clearLink( $this->mTitle ); 00412 00413 if ( $data ) { 00414 $lc->addGoodLinkObjFromRow( $this->mTitle, $data ); 00415 00416 $this->mTitle->loadFromRow( $data ); 00417 00418 // Old-fashioned restrictions 00419 $this->mTitle->loadRestrictions( $data->page_restrictions ); 00420 00421 $this->mId = intval( $data->page_id ); 00422 $this->mCounter = intval( $data->page_counter ); 00423 $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); 00424 $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated ); 00425 $this->mIsRedirect = intval( $data->page_is_redirect ); 00426 $this->mLatest = intval( $data->page_latest ); 00427 // Bug 37225: $latest may no longer match the cached latest Revision object. 00428 // Double-check the ID of any cached latest Revision object for consistency. 00429 if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { 00430 $this->mLastRevision = null; 00431 $this->mTimestamp = ''; 00432 } 00433 } else { 00434 $lc->addBadLinkObj( $this->mTitle ); 00435 00436 $this->mTitle->loadFromRow( false ); 00437 00438 $this->clearCacheFields(); 00439 00440 $this->mId = 0; 00441 } 00442 00443 $this->mDataLoaded = true; 00444 $this->mDataLoadedFrom = self::convertSelectType( $from ); 00445 } 00446 00450 public function getId() { 00451 if ( !$this->mDataLoaded ) { 00452 $this->loadPageData(); 00453 } 00454 return $this->mId; 00455 } 00456 00460 public function exists() { 00461 if ( !$this->mDataLoaded ) { 00462 $this->loadPageData(); 00463 } 00464 return $this->mId > 0; 00465 } 00466 00475 public function hasViewableContent() { 00476 return $this->exists() || $this->mTitle->isAlwaysKnown(); 00477 } 00478 00482 public function getCount() { 00483 if ( !$this->mDataLoaded ) { 00484 $this->loadPageData(); 00485 } 00486 00487 return $this->mCounter; 00488 } 00489 00495 public function isRedirect() { 00496 $content = $this->getContent(); 00497 if ( !$content ) { 00498 return false; 00499 } 00500 00501 return $content->isRedirect(); 00502 } 00503 00514 public function getContentModel() { 00515 if ( $this->exists() ) { 00516 // look at the revision's actual content model 00517 $rev = $this->getRevision(); 00518 00519 if ( $rev !== null ) { 00520 return $rev->getContentModel(); 00521 } else { 00522 $title = $this->mTitle->getPrefixedDBkey(); 00523 wfWarn( "Page $title exists but has no (visible) revisions!" ); 00524 } 00525 } 00526 00527 // use the default model for this page 00528 return $this->mTitle->getContentModel(); 00529 } 00530 00535 public function checkTouched() { 00536 if ( !$this->mDataLoaded ) { 00537 $this->loadPageData(); 00538 } 00539 return !$this->mIsRedirect; 00540 } 00541 00546 public function getTouched() { 00547 if ( !$this->mDataLoaded ) { 00548 $this->loadPageData(); 00549 } 00550 return $this->mTouched; 00551 } 00552 00557 public function getLinksTimestamp() { 00558 if ( !$this->mDataLoaded ) { 00559 $this->loadPageData(); 00560 } 00561 return $this->mLinksUpdated; 00562 } 00563 00568 public function getLatest() { 00569 if ( !$this->mDataLoaded ) { 00570 $this->loadPageData(); 00571 } 00572 return (int)$this->mLatest; 00573 } 00574 00579 public function getOldestRevision() { 00580 wfProfileIn( __METHOD__ ); 00581 00582 // Try using the slave database first, then try the master 00583 $continue = 2; 00584 $db = wfGetDB( DB_SLAVE ); 00585 $revSelectFields = Revision::selectFields(); 00586 00587 $row = null; 00588 while ( $continue ) { 00589 $row = $db->selectRow( 00590 array( 'page', 'revision' ), 00591 $revSelectFields, 00592 array( 00593 'page_namespace' => $this->mTitle->getNamespace(), 00594 'page_title' => $this->mTitle->getDBkey(), 00595 'rev_page = page_id' 00596 ), 00597 __METHOD__, 00598 array( 00599 'ORDER BY' => 'rev_timestamp ASC' 00600 ) 00601 ); 00602 00603 if ( $row ) { 00604 $continue = 0; 00605 } else { 00606 $db = wfGetDB( DB_MASTER ); 00607 $continue--; 00608 } 00609 } 00610 00611 wfProfileOut( __METHOD__ ); 00612 return $row ? Revision::newFromRow( $row ) : null; 00613 } 00614 00619 protected function loadLastEdit() { 00620 if ( $this->mLastRevision !== null ) { 00621 return; // already loaded 00622 } 00623 00624 $latest = $this->getLatest(); 00625 if ( !$latest ) { 00626 return; // page doesn't exist or is missing page_latest info 00627 } 00628 00629 // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the 00630 // latest changes committed. This is true even within REPEATABLE-READ transactions, where 00631 // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to 00632 // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row 00633 // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT. 00634 // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read. 00635 $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0; 00636 $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); 00637 if ( $revision ) { // sanity 00638 $this->setLastEdit( $revision ); 00639 } 00640 } 00641 00646 protected function setLastEdit( Revision $revision ) { 00647 $this->mLastRevision = $revision; 00648 $this->mTimestamp = $revision->getTimestamp(); 00649 } 00650 00655 public function getRevision() { 00656 $this->loadLastEdit(); 00657 if ( $this->mLastRevision ) { 00658 return $this->mLastRevision; 00659 } 00660 return null; 00661 } 00662 00676 public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00677 $this->loadLastEdit(); 00678 if ( $this->mLastRevision ) { 00679 return $this->mLastRevision->getContent( $audience, $user ); 00680 } 00681 return null; 00682 } 00683 00696 public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00697 ContentHandler::deprecated( __METHOD__, '1.21' ); 00698 00699 $this->loadLastEdit(); 00700 if ( $this->mLastRevision ) { 00701 return $this->mLastRevision->getText( $audience, $user ); 00702 } 00703 return false; 00704 } 00705 00712 public function getRawText() { 00713 ContentHandler::deprecated( __METHOD__, '1.21' ); 00714 00715 return $this->getText( Revision::RAW ); 00716 } 00717 00721 public function getTimestamp() { 00722 // Check if the field has been filled by WikiPage::setTimestamp() 00723 if ( !$this->mTimestamp ) { 00724 $this->loadLastEdit(); 00725 } 00726 00727 return wfTimestamp( TS_MW, $this->mTimestamp ); 00728 } 00729 00735 public function setTimestamp( $ts ) { 00736 $this->mTimestamp = wfTimestamp( TS_MW, $ts ); 00737 } 00738 00748 public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00749 $this->loadLastEdit(); 00750 if ( $this->mLastRevision ) { 00751 return $this->mLastRevision->getUser( $audience, $user ); 00752 } else { 00753 return -1; 00754 } 00755 } 00756 00767 public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00768 $revision = $this->getOldestRevision(); 00769 if ( $revision ) { 00770 $userName = $revision->getUserText( $audience, $user ); 00771 return User::newFromName( $userName, false ); 00772 } else { 00773 return null; 00774 } 00775 } 00776 00786 public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00787 $this->loadLastEdit(); 00788 if ( $this->mLastRevision ) { 00789 return $this->mLastRevision->getUserText( $audience, $user ); 00790 } else { 00791 return ''; 00792 } 00793 } 00794 00804 public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) { 00805 $this->loadLastEdit(); 00806 if ( $this->mLastRevision ) { 00807 return $this->mLastRevision->getComment( $audience, $user ); 00808 } else { 00809 return ''; 00810 } 00811 } 00812 00818 public function getMinorEdit() { 00819 $this->loadLastEdit(); 00820 if ( $this->mLastRevision ) { 00821 return $this->mLastRevision->isMinor(); 00822 } else { 00823 return false; 00824 } 00825 } 00826 00832 protected function getCachedLastEditTime() { 00833 global $wgMemc; 00834 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00835 return $wgMemc->get( $key ); 00836 } 00837 00844 public function setCachedLastEditTime( $timestamp ) { 00845 global $wgMemc; 00846 $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) ); 00847 $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60 * 15 ); 00848 } 00849 00858 public function isCountable( $editInfo = false ) { 00859 global $wgArticleCountMethod; 00860 00861 if ( !$this->mTitle->isContentPage() ) { 00862 return false; 00863 } 00864 00865 if ( $editInfo ) { 00866 $content = $editInfo->pstContent; 00867 } else { 00868 $content = $this->getContent(); 00869 } 00870 00871 if ( !$content || $content->isRedirect() ) { 00872 return false; 00873 } 00874 00875 $hasLinks = null; 00876 00877 if ( $wgArticleCountMethod === 'link' ) { 00878 // nasty special case to avoid re-parsing to detect links 00879 00880 if ( $editInfo ) { 00881 // ParserOutput::getLinks() is a 2D array of page links, so 00882 // to be really correct we would need to recurse in the array 00883 // but the main array should only have items in it if there are 00884 // links. 00885 $hasLinks = (bool)count( $editInfo->output->getLinks() ); 00886 } else { 00887 $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1, 00888 array( 'pl_from' => $this->getId() ), __METHOD__ ); 00889 } 00890 } 00891 00892 return $content->isCountable( $hasLinks ); 00893 } 00894 00902 public function getRedirectTarget() { 00903 if ( !$this->mTitle->isRedirect() ) { 00904 return null; 00905 } 00906 00907 if ( $this->mRedirectTarget !== null ) { 00908 return $this->mRedirectTarget; 00909 } 00910 00911 // Query the redirect table 00912 $dbr = wfGetDB( DB_SLAVE ); 00913 $row = $dbr->selectRow( 'redirect', 00914 array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), 00915 array( 'rd_from' => $this->getId() ), 00916 __METHOD__ 00917 ); 00918 00919 // rd_fragment and rd_interwiki were added later, populate them if empty 00920 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { 00921 $this->mRedirectTarget = Title::makeTitle( 00922 $row->rd_namespace, $row->rd_title, 00923 $row->rd_fragment, $row->rd_interwiki ); 00924 return $this->mRedirectTarget; 00925 } 00926 00927 // This page doesn't have an entry in the redirect table 00928 $this->mRedirectTarget = $this->insertRedirect(); 00929 return $this->mRedirectTarget; 00930 } 00931 00938 public function insertRedirect() { 00939 // recurse through to only get the final target 00940 $content = $this->getContent(); 00941 $retval = $content ? $content->getUltimateRedirectTarget() : null; 00942 if ( !$retval ) { 00943 return null; 00944 } 00945 $this->insertRedirectEntry( $retval ); 00946 return $retval; 00947 } 00948 00954 public function insertRedirectEntry( $rt ) { 00955 $dbw = wfGetDB( DB_MASTER ); 00956 $dbw->replace( 'redirect', array( 'rd_from' ), 00957 array( 00958 'rd_from' => $this->getId(), 00959 'rd_namespace' => $rt->getNamespace(), 00960 'rd_title' => $rt->getDBkey(), 00961 'rd_fragment' => $rt->getFragment(), 00962 'rd_interwiki' => $rt->getInterwiki(), 00963 ), 00964 __METHOD__ 00965 ); 00966 } 00967 00973 public function followRedirect() { 00974 return $this->getRedirectURL( $this->getRedirectTarget() ); 00975 } 00976 00984 public function getRedirectURL( $rt ) { 00985 if ( !$rt ) { 00986 return false; 00987 } 00988 00989 if ( $rt->isExternal() ) { 00990 if ( $rt->isLocal() ) { 00991 // Offsite wikis need an HTTP redirect. 00992 // 00993 // This can be hard to reverse and may produce loops, 00994 // so they may be disabled in the site configuration. 00995 $source = $this->mTitle->getFullURL( 'redirect=no' ); 00996 return $rt->getFullURL( array( 'rdfrom' => $source ) ); 00997 } else { 00998 // External pages pages without "local" bit set are not valid 00999 // redirect targets 01000 return false; 01001 } 01002 } 01003 01004 if ( $rt->isSpecialPage() ) { 01005 // Gotta handle redirects to special pages differently: 01006 // Fill the HTTP response "Location" header and ignore 01007 // the rest of the page we're on. 01008 // 01009 // Some pages are not valid targets 01010 if ( $rt->isValidRedirectTarget() ) { 01011 return $rt->getFullURL(); 01012 } else { 01013 return false; 01014 } 01015 } 01016 01017 return $rt; 01018 } 01019 01025 public function getContributors() { 01026 // @todo FIXME: This is expensive; cache this info somewhere. 01027 01028 $dbr = wfGetDB( DB_SLAVE ); 01029 01030 if ( $dbr->implicitGroupby() ) { 01031 $realNameField = 'user_real_name'; 01032 } else { 01033 $realNameField = 'MIN(user_real_name) AS user_real_name'; 01034 } 01035 01036 $tables = array( 'revision', 'user' ); 01037 01038 $fields = array( 01039 'user_id' => 'rev_user', 01040 'user_name' => 'rev_user_text', 01041 $realNameField, 01042 'timestamp' => 'MAX(rev_timestamp)', 01043 ); 01044 01045 $conds = array( 'rev_page' => $this->getId() ); 01046 01047 // The user who made the top revision gets credited as "this page was last edited by 01048 // John, based on contributions by Tom, Dick and Harry", so don't include them twice. 01049 $user = $this->getUser(); 01050 if ( $user ) { 01051 $conds[] = "rev_user != $user"; 01052 } else { 01053 $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}"; 01054 } 01055 01056 $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden? 01057 01058 $jconds = array( 01059 'user' => array( 'LEFT JOIN', 'rev_user = user_id' ), 01060 ); 01061 01062 $options = array( 01063 'GROUP BY' => array( 'rev_user', 'rev_user_text' ), 01064 'ORDER BY' => 'timestamp DESC', 01065 ); 01066 01067 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds ); 01068 return new UserArrayFromResult( $res ); 01069 } 01070 01077 public function getLastNAuthors( $num, $revLatest = 0 ) { 01078 wfProfileIn( __METHOD__ ); 01079 // First try the slave 01080 // If that doesn't have the latest revision, try the master 01081 $continue = 2; 01082 $db = wfGetDB( DB_SLAVE ); 01083 01084 do { 01085 $res = $db->select( array( 'page', 'revision' ), 01086 array( 'rev_id', 'rev_user_text' ), 01087 array( 01088 'page_namespace' => $this->mTitle->getNamespace(), 01089 'page_title' => $this->mTitle->getDBkey(), 01090 'rev_page = page_id' 01091 ), __METHOD__, 01092 array( 01093 'ORDER BY' => 'rev_timestamp DESC', 01094 'LIMIT' => $num 01095 ) 01096 ); 01097 01098 if ( !$res ) { 01099 wfProfileOut( __METHOD__ ); 01100 return array(); 01101 } 01102 01103 $row = $db->fetchObject( $res ); 01104 01105 if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { 01106 $db = wfGetDB( DB_MASTER ); 01107 $continue--; 01108 } else { 01109 $continue = 0; 01110 } 01111 } while ( $continue ); 01112 01113 $authors = array( $row->rev_user_text ); 01114 01115 foreach ( $res as $row ) { 01116 $authors[] = $row->rev_user_text; 01117 } 01118 01119 wfProfileOut( __METHOD__ ); 01120 return $authors; 01121 } 01122 01130 public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { 01131 global $wgEnableParserCache; 01132 01133 return $wgEnableParserCache 01134 && $parserOptions->getStubThreshold() == 0 01135 && $this->exists() 01136 && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) 01137 && $this->getContentHandler()->isParserCacheSupported(); 01138 } 01139 01151 public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { 01152 wfProfileIn( __METHOD__ ); 01153 01154 $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); 01155 wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); 01156 if ( $parserOptions->getStubThreshold() ) { 01157 wfIncrStats( 'pcache_miss_stub' ); 01158 } 01159 01160 if ( $useParserCache ) { 01161 $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); 01162 if ( $parserOutput !== false ) { 01163 wfProfileOut( __METHOD__ ); 01164 return $parserOutput; 01165 } 01166 } 01167 01168 if ( $oldid === null || $oldid === 0 ) { 01169 $oldid = $this->getLatest(); 01170 } 01171 01172 $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache ); 01173 $pool->execute(); 01174 01175 wfProfileOut( __METHOD__ ); 01176 01177 return $pool->getParserOutput(); 01178 } 01179 01185 public function doViewUpdates( User $user, $oldid = 0 ) { 01186 global $wgDisableCounters; 01187 if ( wfReadOnly() ) { 01188 return; 01189 } 01190 01191 // Don't update page view counters on views from bot users (bug 14044) 01192 if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->exists() ) { 01193 DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) ); 01194 DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) ); 01195 } 01196 01197 // Update newtalk / watchlist notification status 01198 $user->clearNotification( $this->mTitle, $oldid ); 01199 } 01200 01205 public function doPurge() { 01206 global $wgUseSquid; 01207 01208 if ( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) { 01209 return false; 01210 } 01211 01212 // Invalidate the cache 01213 $this->mTitle->invalidateCache(); 01214 01215 if ( $wgUseSquid ) { 01216 // Commit the transaction before the purge is sent 01217 $dbw = wfGetDB( DB_MASTER ); 01218 $dbw->commit( __METHOD__ ); 01219 01220 // Send purge 01221 $update = SquidUpdate::newSimplePurge( $this->mTitle ); 01222 $update->doUpdate(); 01223 } 01224 01225 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 01226 // @todo move this logic to MessageCache 01227 01228 if ( $this->exists() ) { 01229 // NOTE: use transclusion text for messages. 01230 // This is consistent with MessageCache::getMsgFromNamespace() 01231 01232 $content = $this->getContent(); 01233 $text = $content === null ? null : $content->getWikitextForTransclusion(); 01234 01235 if ( $text === null ) { 01236 $text = false; 01237 } 01238 } else { 01239 $text = false; 01240 } 01241 01242 MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); 01243 } 01244 return true; 01245 } 01246 01257 public function insertOn( $dbw ) { 01258 wfProfileIn( __METHOD__ ); 01259 01260 $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' ); 01261 $dbw->insert( 'page', array( 01262 'page_id' => $page_id, 01263 'page_namespace' => $this->mTitle->getNamespace(), 01264 'page_title' => $this->mTitle->getDBkey(), 01265 'page_counter' => 0, 01266 'page_restrictions' => '', 01267 'page_is_redirect' => 0, // Will set this shortly... 01268 'page_is_new' => 1, 01269 'page_random' => wfRandom(), 01270 'page_touched' => $dbw->timestamp(), 01271 'page_latest' => 0, // Fill this in shortly... 01272 'page_len' => 0, // Fill this in shortly... 01273 ), __METHOD__, 'IGNORE' ); 01274 01275 $affected = $dbw->affectedRows(); 01276 01277 if ( $affected ) { 01278 $newid = $dbw->insertId(); 01279 $this->mId = $newid; 01280 $this->mTitle->resetArticleID( $newid ); 01281 } 01282 wfProfileOut( __METHOD__ ); 01283 01284 return $affected ? $newid : false; 01285 } 01286 01300 public function updateRevisionOn( $dbw, $revision, $lastRevision = null, 01301 $lastRevIsRedirect = null 01302 ) { 01303 global $wgContentHandlerUseDB; 01304 01305 wfProfileIn( __METHOD__ ); 01306 01307 $content = $revision->getContent(); 01308 $len = $content ? $content->getSize() : 0; 01309 $rt = $content ? $content->getUltimateRedirectTarget() : null; 01310 01311 $conditions = array( 'page_id' => $this->getId() ); 01312 01313 if ( !is_null( $lastRevision ) ) { 01314 // An extra check against threads stepping on each other 01315 $conditions['page_latest'] = $lastRevision; 01316 } 01317 01318 $now = wfTimestampNow(); 01319 $row = array( /* SET */ 01320 'page_latest' => $revision->getId(), 01321 'page_touched' => $dbw->timestamp( $now ), 01322 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, 01323 'page_is_redirect' => $rt !== null ? 1 : 0, 01324 'page_len' => $len, 01325 ); 01326 01327 if ( $wgContentHandlerUseDB ) { 01328 $row['page_content_model'] = $revision->getContentModel(); 01329 } 01330 01331 $dbw->update( 'page', 01332 $row, 01333 $conditions, 01334 __METHOD__ ); 01335 01336 $result = $dbw->affectedRows() > 0; 01337 if ( $result ) { 01338 $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect ); 01339 $this->setLastEdit( $revision ); 01340 $this->setCachedLastEditTime( $now ); 01341 $this->mLatest = $revision->getId(); 01342 $this->mIsRedirect = (bool)$rt; 01343 // Update the LinkCache. 01344 LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, 01345 $this->mLatest, $revision->getContentModel() ); 01346 } 01347 01348 wfProfileOut( __METHOD__ ); 01349 return $result; 01350 } 01351 01363 public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) { 01364 // Always update redirects (target link might have changed) 01365 // Update/Insert if we don't know if the last revision was a redirect or not 01366 // Delete if changing from redirect to non-redirect 01367 $isRedirect = !is_null( $redirectTitle ); 01368 01369 if ( !$isRedirect && $lastRevIsRedirect === false ) { 01370 return true; 01371 } 01372 01373 wfProfileIn( __METHOD__ ); 01374 if ( $isRedirect ) { 01375 $this->insertRedirectEntry( $redirectTitle ); 01376 } else { 01377 // This is not a redirect, remove row from redirect table 01378 $where = array( 'rd_from' => $this->getId() ); 01379 $dbw->delete( 'redirect', $where, __METHOD__ ); 01380 } 01381 01382 if ( $this->getTitle()->getNamespace() == NS_FILE ) { 01383 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() ); 01384 } 01385 wfProfileOut( __METHOD__ ); 01386 01387 return ( $dbw->affectedRows() != 0 ); 01388 } 01389 01400 public function updateIfNewerOn( $dbw, $revision ) { 01401 wfProfileIn( __METHOD__ ); 01402 01403 $row = $dbw->selectRow( 01404 array( 'revision', 'page' ), 01405 array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ), 01406 array( 01407 'page_id' => $this->getId(), 01408 'page_latest=rev_id' ), 01409 __METHOD__ ); 01410 01411 if ( $row ) { 01412 if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) { 01413 wfProfileOut( __METHOD__ ); 01414 return false; 01415 } 01416 $prev = $row->rev_id; 01417 $lastRevIsRedirect = (bool)$row->page_is_redirect; 01418 } else { 01419 // No or missing previous revision; mark the page as new 01420 $prev = 0; 01421 $lastRevIsRedirect = null; 01422 } 01423 01424 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect ); 01425 01426 wfProfileOut( __METHOD__ ); 01427 return $ret; 01428 } 01429 01440 public function getUndoContent( Revision $undo, Revision $undoafter = null ) { 01441 $handler = $undo->getContentHandler(); 01442 return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter ); 01443 } 01444 01454 public function getUndoText( Revision $undo, Revision $undoafter = null ) { 01455 ContentHandler::deprecated( __METHOD__, '1.21' ); 01456 01457 $this->loadLastEdit(); 01458 01459 if ( $this->mLastRevision ) { 01460 if ( is_null( $undoafter ) ) { 01461 $undoafter = $undo->getPrevious(); 01462 } 01463 01464 $handler = $this->getContentHandler(); 01465 $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter ); 01466 01467 if ( !$undone ) { 01468 return false; 01469 } else { 01470 return ContentHandler::getContentText( $undone ); 01471 } 01472 } 01473 01474 return false; 01475 } 01476 01490 public function replaceSection( $sectionId, $text, $sectionTitle = '', 01491 $edittime = null 01492 ) { 01493 ContentHandler::deprecated( __METHOD__, '1.21' ); 01494 01495 //NOTE: keep condition in sync with condition in replaceSectionContent! 01496 if ( strval( $sectionId ) === '' ) { 01497 // Whole-page edit; let the whole text through 01498 return $text; 01499 } 01500 01501 if ( !$this->supportsSections() ) { 01502 throw new MWException( "sections not supported for content model " . 01503 $this->getContentHandler()->getModelID() ); 01504 } 01505 01506 // could even make section title, but that's not required. 01507 $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); 01508 01509 $newContent = $this->replaceSectionContent( $sectionId, $sectionContent, $sectionTitle, 01510 $edittime ); 01511 01512 return ContentHandler::getContentText( $newContent ); 01513 } 01514 01525 public function supportsSections() { 01526 return $this->getContentHandler()->supportsSections(); 01527 } 01528 01543 public function replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle = '', 01544 $edittime = null ) { 01545 wfProfileIn( __METHOD__ ); 01546 01547 $baseRevId = null; 01548 if ( $edittime && $sectionId !== 'new' ) { 01549 $dbw = wfGetDB( DB_MASTER ); 01550 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); 01551 if ( $rev ) { 01552 $baseRevId = $rev->getId(); 01553 } 01554 } 01555 01556 wfProfileOut( __METHOD__ ); 01557 return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId ); 01558 } 01559 01573 public function replaceSectionAtRev( $sectionId, Content $sectionContent, 01574 $sectionTitle = '', $baseRevId = null 01575 ) { 01576 wfProfileIn( __METHOD__ ); 01577 01578 if ( strval( $sectionId ) === '' ) { 01579 // Whole-page edit; let the whole text through 01580 $newContent = $sectionContent; 01581 } else { 01582 if ( !$this->supportsSections() ) { 01583 wfProfileOut( __METHOD__ ); 01584 throw new MWException( "sections not supported for content model " . 01585 $this->getContentHandler()->getModelID() ); 01586 } 01587 01588 // Bug 30711: always use current version when adding a new section 01589 if ( is_null( $baseRevId ) || $sectionId === 'new' ) { 01590 $oldContent = $this->getContent(); 01591 } else { 01592 // TODO: try DB_SLAVE first 01593 $dbw = wfGetDB( DB_MASTER ); 01594 $rev = Revision::loadFromId( $dbw, $baseRevId ); 01595 01596 if ( !$rev ) { 01597 wfDebug( __METHOD__ . " asked for bogus section (page: " . 01598 $this->getId() . "; section: $sectionId)\n" ); 01599 wfProfileOut( __METHOD__ ); 01600 return null; 01601 } 01602 01603 $oldContent = $rev->getContent(); 01604 } 01605 01606 if ( !$oldContent ) { 01607 wfDebug( __METHOD__ . ": no page text\n" ); 01608 wfProfileOut( __METHOD__ ); 01609 return null; 01610 } 01611 01612 $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle ); 01613 } 01614 01615 wfProfileOut( __METHOD__ ); 01616 return $newContent; 01617 } 01618 01624 public function checkFlags( $flags ) { 01625 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) { 01626 if ( $this->exists() ) { 01627 $flags |= EDIT_UPDATE; 01628 } else { 01629 $flags |= EDIT_NEW; 01630 } 01631 } 01632 01633 return $flags; 01634 } 01635 01688 public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { 01689 ContentHandler::deprecated( __METHOD__, '1.21' ); 01690 01691 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 01692 01693 return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user ); 01694 } 01695 01747 public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, 01748 User $user = null, $serialisation_format = null 01749 ) { 01750 global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol; 01751 01752 // Low-level sanity check 01753 if ( $this->mTitle->getText() === '' ) { 01754 throw new MWException( 'Something is trying to edit an article with an empty title' ); 01755 } 01756 01757 wfProfileIn( __METHOD__ ); 01758 01759 if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) { 01760 wfProfileOut( __METHOD__ ); 01761 return Status::newFatal( 'content-not-allowed-here', 01762 ContentHandler::getLocalizedName( $content->getModel() ), 01763 $this->getTitle()->getPrefixedText() ); 01764 } 01765 01766 $user = is_null( $user ) ? $wgUser : $user; 01767 $status = Status::newGood( array() ); 01768 01769 // Load the data from the master database if needed. 01770 // The caller may already loaded it from the master or even loaded it using 01771 // SELECT FOR UPDATE, so do not override that using clear(). 01772 $this->loadPageData( 'fromdbmaster' ); 01773 01774 $flags = $this->checkFlags( $flags ); 01775 01776 // handle hook 01777 $hook_args = array( &$this, &$user, &$content, &$summary, 01778 $flags & EDIT_MINOR, null, null, &$flags, &$status ); 01779 01780 if ( !wfRunHooks( 'PageContentSave', $hook_args ) 01781 || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) { 01782 01783 wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" ); 01784 01785 if ( $status->isOK() ) { 01786 $status->fatal( 'edit-hook-aborted' ); 01787 } 01788 01789 wfProfileOut( __METHOD__ ); 01790 return $status; 01791 } 01792 01793 // Silently ignore EDIT_MINOR if not allowed 01794 $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ); 01795 $bot = $flags & EDIT_FORCE_BOT; 01796 01797 $old_content = $this->getContent( Revision::RAW ); // current revision's content 01798 01799 $oldsize = $old_content ? $old_content->getSize() : 0; 01800 $oldid = $this->getLatest(); 01801 $oldIsRedirect = $this->isRedirect(); 01802 $oldcountable = $this->isCountable(); 01803 01804 $handler = $content->getContentHandler(); 01805 01806 // Provide autosummaries if one is not provided and autosummaries are enabled. 01807 if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) { 01808 if ( !$old_content ) { 01809 $old_content = null; 01810 } 01811 $summary = $handler->getAutosummary( $old_content, $content, $flags ); 01812 } 01813 01814 $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format ); 01815 $serialized = $editInfo->pst; 01816 01820 $content = $editInfo->pstContent; 01821 $newsize = $content->getSize(); 01822 01823 $dbw = wfGetDB( DB_MASTER ); 01824 $now = wfTimestampNow(); 01825 $this->mTimestamp = $now; 01826 01827 if ( $flags & EDIT_UPDATE ) { 01828 // Update article, but only if changed. 01829 $status->value['new'] = false; 01830 01831 if ( !$oldid ) { 01832 // Article gone missing 01833 wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" ); 01834 $status->fatal( 'edit-gone-missing' ); 01835 01836 wfProfileOut( __METHOD__ ); 01837 return $status; 01838 } elseif ( !$old_content ) { 01839 // Sanity check for bug 37225 01840 wfProfileOut( __METHOD__ ); 01841 throw new MWException( "Could not find text for current revision {$oldid}." ); 01842 } 01843 01844 $revision = new Revision( array( 01845 'page' => $this->getId(), 01846 'title' => $this->getTitle(), // for determining the default content model 01847 'comment' => $summary, 01848 'minor_edit' => $isminor, 01849 'text' => $serialized, 01850 'len' => $newsize, 01851 'parent_id' => $oldid, 01852 'user' => $user->getId(), 01853 'user_text' => $user->getName(), 01854 'timestamp' => $now, 01855 'content_model' => $content->getModel(), 01856 'content_format' => $serialisation_format, 01857 ) ); // XXX: pass content object?! 01858 01859 $changed = !$content->equals( $old_content ); 01860 01861 if ( $changed ) { 01862 if ( !$content->isValid() ) { 01863 wfProfileOut( __METHOD__ ); 01864 throw new MWException( "New content failed validity check!" ); 01865 } 01866 01867 $dbw->begin( __METHOD__ ); 01868 try { 01869 01870 $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); 01871 $status->merge( $prepStatus ); 01872 01873 if ( !$status->isOK() ) { 01874 $dbw->rollback( __METHOD__ ); 01875 01876 wfProfileOut( __METHOD__ ); 01877 return $status; 01878 } 01879 $revisionId = $revision->insertOn( $dbw ); 01880 01881 // Update page 01882 // 01883 // We check for conflicts by comparing $oldid with the current latest revision ID. 01884 $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); 01885 01886 if ( !$ok ) { 01887 // Belated edit conflict! Run away!! 01888 $status->fatal( 'edit-conflict' ); 01889 01890 $dbw->rollback( __METHOD__ ); 01891 01892 wfProfileOut( __METHOD__ ); 01893 return $status; 01894 } 01895 01896 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); 01897 // Update recentchanges 01898 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 01899 // Mark as patrolled if the user can do so 01900 $patrolled = $wgUseRCPatrol && !count( 01901 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 01902 // Add RC row to the DB 01903 $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, 01904 $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, 01905 $revisionId, $patrolled 01906 ); 01907 01908 // Log auto-patrolled edits 01909 if ( $patrolled ) { 01910 PatrolLog::record( $rc, true, $user ); 01911 } 01912 } 01913 $user->incEditCount(); 01914 } catch ( MWException $e ) { 01915 $dbw->rollback( __METHOD__ ); 01916 // Question: Would it perhaps be better if this method turned all 01917 // exceptions into $status's? 01918 throw $e; 01919 } 01920 $dbw->commit( __METHOD__ ); 01921 } else { 01922 // Bug 32948: revision ID must be set to page {{REVISIONID}} and 01923 // related variables correctly 01924 $revision->setId( $this->getLatest() ); 01925 } 01926 01927 // Update links tables, site stats, etc. 01928 $this->doEditUpdates( 01929 $revision, 01930 $user, 01931 array( 01932 'changed' => $changed, 01933 'oldcountable' => $oldcountable 01934 ) 01935 ); 01936 01937 if ( !$changed ) { 01938 $status->warning( 'edit-no-change' ); 01939 $revision = null; 01940 // Update page_touched, this is usually implicit in the page update 01941 // Other cache updates are done in onArticleEdit() 01942 $this->mTitle->invalidateCache(); 01943 } 01944 } else { 01945 // Create new article 01946 $status->value['new'] = true; 01947 01948 $dbw->begin( __METHOD__ ); 01949 try { 01950 01951 $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user ); 01952 $status->merge( $prepStatus ); 01953 01954 if ( !$status->isOK() ) { 01955 $dbw->rollback( __METHOD__ ); 01956 01957 wfProfileOut( __METHOD__ ); 01958 return $status; 01959 } 01960 01961 $status->merge( $prepStatus ); 01962 01963 // Add the page record; stake our claim on this title! 01964 // This will return false if the article already exists 01965 $newid = $this->insertOn( $dbw ); 01966 01967 if ( $newid === false ) { 01968 $dbw->rollback( __METHOD__ ); 01969 $status->fatal( 'edit-already-exists' ); 01970 01971 wfProfileOut( __METHOD__ ); 01972 return $status; 01973 } 01974 01975 // Save the revision text... 01976 $revision = new Revision( array( 01977 'page' => $newid, 01978 'title' => $this->getTitle(), // for determining the default content model 01979 'comment' => $summary, 01980 'minor_edit' => $isminor, 01981 'text' => $serialized, 01982 'len' => $newsize, 01983 'user' => $user->getId(), 01984 'user_text' => $user->getName(), 01985 'timestamp' => $now, 01986 'content_model' => $content->getModel(), 01987 'content_format' => $serialisation_format, 01988 ) ); 01989 $revisionId = $revision->insertOn( $dbw ); 01990 01991 // Bug 37225: use accessor to get the text as Revision may trim it 01992 $content = $revision->getContent(); // sanity; get normalized version 01993 01994 if ( $content ) { 01995 $newsize = $content->getSize(); 01996 } 01997 01998 // Update the page record with revision data 01999 $this->updateRevisionOn( $dbw, $revision, 0 ); 02000 02001 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 02002 02003 // Update recentchanges 02004 if ( !( $flags & EDIT_SUPPRESS_RC ) ) { 02005 // Mark as patrolled if the user can do so 02006 $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( 02007 $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); 02008 // Add RC row to the DB 02009 $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, 02010 '', $newsize, $revisionId, $patrolled ); 02011 02012 // Log auto-patrolled edits 02013 if ( $patrolled ) { 02014 PatrolLog::record( $rc, true, $user ); 02015 } 02016 } 02017 $user->incEditCount(); 02018 02019 } catch ( MWException $e ) { 02020 $dbw->rollback( __METHOD__ ); 02021 throw $e; 02022 } 02023 $dbw->commit( __METHOD__ ); 02024 02025 // Update links, etc. 02026 $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); 02027 02028 $hook_args = array( &$this, &$user, $content, $summary, 02029 $flags & EDIT_MINOR, null, null, &$flags, $revision ); 02030 02031 ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args ); 02032 wfRunHooks( 'PageContentInsertComplete', $hook_args ); 02033 } 02034 02035 // Do updates right now unless deferral was requested 02036 if ( !( $flags & EDIT_DEFER_UPDATES ) ) { 02037 DeferredUpdates::doUpdates(); 02038 } 02039 02040 // Return the new revision (or null) to the caller 02041 $status->value['revision'] = $revision; 02042 02043 $hook_args = array( &$this, &$user, $content, $summary, 02044 $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); 02045 02046 ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); 02047 wfRunHooks( 'PageContentSaveComplete', $hook_args ); 02048 02049 // Promote user to any groups they meet the criteria for 02050 $dbw->onTransactionIdle( function () use ( $user ) { 02051 $user->addAutopromoteOnceGroups( 'onEdit' ); 02052 } ); 02053 02054 wfProfileOut( __METHOD__ ); 02055 return $status; 02056 } 02057 02072 public function makeParserOptions( $context ) { 02073 $options = $this->getContentHandler()->makeParserOptions( $context ); 02074 02075 if ( $this->getTitle()->isConversionTable() ) { 02076 // @todo ConversionTable should become a separate content model, so 02077 // we don't need special cases like this one. 02078 $options->disableContentConversion(); 02079 } 02080 02081 return $options; 02082 } 02083 02091 public function prepareTextForEdit( $text, $revid = null, User $user = null ) { 02092 ContentHandler::deprecated( __METHOD__, '1.21' ); 02093 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 02094 return $this->prepareContentForEdit( $content, $revid, $user ); 02095 } 02096 02110 public function prepareContentForEdit( Content $content, $revid = null, User $user = null, 02111 $serialization_format = null 02112 ) { 02113 global $wgContLang, $wgUser; 02114 $user = is_null( $user ) ? $wgUser : $user; 02115 //XXX: check $user->getId() here??? 02116 02117 // Use a sane default for $serialization_format, see bug 57026 02118 if ( $serialization_format === null ) { 02119 $serialization_format = $content->getContentHandler()->getDefaultFormat(); 02120 } 02121 02122 if ( $this->mPreparedEdit 02123 && $this->mPreparedEdit->newContent 02124 && $this->mPreparedEdit->newContent->equals( $content ) 02125 && $this->mPreparedEdit->revid == $revid 02126 && $this->mPreparedEdit->format == $serialization_format 02127 // XXX: also check $user here? 02128 ) { 02129 // Already prepared 02130 return $this->mPreparedEdit; 02131 } 02132 02133 $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); 02134 wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) ); 02135 02136 $edit = (object)array(); 02137 $edit->revid = $revid; 02138 $edit->timestamp = wfTimestampNow(); 02139 02140 $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null; 02141 02142 $edit->format = $serialization_format; 02143 $edit->popts = $this->makeParserOptions( 'canonical' ); 02144 $edit->output = $edit->pstContent 02145 ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts ) 02146 : null; 02147 02148 $edit->newContent = $content; 02149 $edit->oldContent = $this->getContent( Revision::RAW ); 02150 02151 // NOTE: B/C for hooks! don't use these fields! 02152 $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : ''; 02153 $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; 02154 $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : ''; 02155 02156 $this->mPreparedEdit = $edit; 02157 return $edit; 02158 } 02159 02176 public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { 02177 global $wgEnableParserCache; 02178 02179 wfProfileIn( __METHOD__ ); 02180 02181 $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null ); 02182 $content = $revision->getContent(); 02183 02184 // Parse the text 02185 // Be careful not to do pre-save transform twice: $text is usually 02186 // already pre-save transformed once. 02187 if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) { 02188 wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" ); 02189 $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user ); 02190 } else { 02191 wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" ); 02192 $editInfo = $this->mPreparedEdit; 02193 } 02194 02195 // Save it to the parser cache 02196 if ( $wgEnableParserCache ) { 02197 $parserCache = ParserCache::singleton(); 02198 $parserCache->save( 02199 $editInfo->output, $this, $editInfo->popts, $editInfo->timestamp, $editInfo->revid 02200 ); 02201 } 02202 02203 // Update the links tables and other secondary data 02204 if ( $content ) { 02205 $recursive = $options['changed']; // bug 50785 02206 $updates = $content->getSecondaryDataUpdates( 02207 $this->getTitle(), null, $recursive, $editInfo->output ); 02208 DataUpdate::runUpdates( $updates ); 02209 } 02210 02211 wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); 02212 02213 if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { 02214 if ( 0 == mt_rand( 0, 99 ) ) { 02215 // Flush old entries from the `recentchanges` table; we do this on 02216 // random requests so as to avoid an increase in writes for no good reason 02217 RecentChange::purgeExpiredChanges(); 02218 } 02219 } 02220 02221 if ( !$this->exists() ) { 02222 wfProfileOut( __METHOD__ ); 02223 return; 02224 } 02225 02226 $id = $this->getId(); 02227 $title = $this->mTitle->getPrefixedDBkey(); 02228 $shortTitle = $this->mTitle->getDBkey(); 02229 02230 if ( !$options['changed'] ) { 02231 $good = 0; 02232 } elseif ( $options['created'] ) { 02233 $good = (int)$this->isCountable( $editInfo ); 02234 } elseif ( $options['oldcountable'] !== null ) { 02235 $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable']; 02236 } else { 02237 $good = 0; 02238 } 02239 $edits = $options['changed'] ? 1 : 0; 02240 $total = $options['created'] ? 1 : 0; 02241 02242 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) ); 02243 DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) ); 02244 02245 // If this is another user's talk page, update newtalk. 02246 // Don't do this if $options['changed'] = false (null-edits) nor if 02247 // it's a minor edit and the user doesn't want notifications for those. 02248 if ( $options['changed'] 02249 && $this->mTitle->getNamespace() == NS_USER_TALK 02250 && $shortTitle != $user->getTitleKey() 02251 && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) ) 02252 ) { 02253 $recipient = User::newFromName( $shortTitle, false ); 02254 if ( !$recipient ) { 02255 wfDebug( __METHOD__ . ": invalid username\n" ); 02256 } else { 02257 // Allow extensions to prevent user notification when a new message is added to their talk page 02258 if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) { 02259 if ( User::isIP( $shortTitle ) ) { 02260 // An anonymous user 02261 $recipient->setNewtalk( true, $revision ); 02262 } elseif ( $recipient->isLoggedIn() ) { 02263 $recipient->setNewtalk( true, $revision ); 02264 } else { 02265 wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" ); 02266 } 02267 } 02268 } 02269 } 02270 02271 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { 02272 // XXX: could skip pseudo-messages like js/css here, based on content model. 02273 $msgtext = $content ? $content->getWikitextForTransclusion() : null; 02274 if ( $msgtext === false || $msgtext === null ) { 02275 $msgtext = ''; 02276 } 02277 02278 MessageCache::singleton()->replace( $shortTitle, $msgtext ); 02279 } 02280 02281 if ( $options['created'] ) { 02282 self::onArticleCreate( $this->mTitle ); 02283 } elseif ( $options['changed'] ) { // bug 50785 02284 self::onArticleEdit( $this->mTitle ); 02285 } 02286 02287 wfProfileOut( __METHOD__ ); 02288 } 02289 02302 public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) { 02303 ContentHandler::deprecated( __METHOD__, "1.21" ); 02304 02305 $content = ContentHandler::makeContent( $text, $this->getTitle() ); 02306 $this->doQuickEditContent( $content, $user, $comment, $minor ); 02307 } 02308 02320 public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false, 02321 $serialisation_format = null 02322 ) { 02323 wfProfileIn( __METHOD__ ); 02324 02325 $serialized = $content->serialize( $serialisation_format ); 02326 02327 $dbw = wfGetDB( DB_MASTER ); 02328 $revision = new Revision( array( 02329 'title' => $this->getTitle(), // for determining the default content model 02330 'page' => $this->getId(), 02331 'user_text' => $user->getName(), 02332 'user' => $user->getId(), 02333 'text' => $serialized, 02334 'length' => $content->getSize(), 02335 'comment' => $comment, 02336 'minor_edit' => $minor ? 1 : 0, 02337 ) ); // XXX: set the content object? 02338 $revision->insertOn( $dbw ); 02339 $this->updateRevisionOn( $dbw, $revision ); 02340 02341 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); 02342 02343 wfProfileOut( __METHOD__ ); 02344 } 02345 02357 public function doUpdateRestrictions( array $limit, array $expiry, 02358 &$cascade, $reason, User $user 02359 ) { 02360 global $wgCascadingRestrictionLevels, $wgContLang; 02361 02362 if ( wfReadOnly() ) { 02363 return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); 02364 } 02365 02366 $this->loadPageData( 'fromdbmaster' ); 02367 $restrictionTypes = $this->mTitle->getRestrictionTypes(); 02368 $id = $this->getId(); 02369 02370 if ( !$cascade ) { 02371 $cascade = false; 02372 } 02373 02374 // Take this opportunity to purge out expired restrictions 02375 Title::purgeExpiredRestrictions(); 02376 02377 // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37); 02378 // we expect a single selection, but the schema allows otherwise. 02379 $isProtected = false; 02380 $protect = false; 02381 $changed = false; 02382 02383 $dbw = wfGetDB( DB_MASTER ); 02384 02385 foreach ( $restrictionTypes as $action ) { 02386 if ( !isset( $expiry[$action] ) ) { 02387 $expiry[$action] = $dbw->getInfinity(); 02388 } 02389 if ( !isset( $limit[$action] ) ) { 02390 $limit[$action] = ''; 02391 } elseif ( $limit[$action] != '' ) { 02392 $protect = true; 02393 } 02394 02395 // Get current restrictions on $action 02396 $current = implode( '', $this->mTitle->getRestrictions( $action ) ); 02397 if ( $current != '' ) { 02398 $isProtected = true; 02399 } 02400 02401 if ( $limit[$action] != $current ) { 02402 $changed = true; 02403 } elseif ( $limit[$action] != '' ) { 02404 // Only check expiry change if the action is actually being 02405 // protected, since expiry does nothing on an not-protected 02406 // action. 02407 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) { 02408 $changed = true; 02409 } 02410 } 02411 } 02412 02413 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) { 02414 $changed = true; 02415 } 02416 02417 // If nothing has changed, do nothing 02418 if ( !$changed ) { 02419 return Status::newGood(); 02420 } 02421 02422 if ( !$protect ) { // No protection at all means unprotection 02423 $revCommentMsg = 'unprotectedarticle'; 02424 $logAction = 'unprotect'; 02425 } elseif ( $isProtected ) { 02426 $revCommentMsg = 'modifiedarticleprotection'; 02427 $logAction = 'modify'; 02428 } else { 02429 $revCommentMsg = 'protectedarticle'; 02430 $logAction = 'protect'; 02431 } 02432 02433 // Truncate for whole multibyte characters 02434 $reason = $wgContLang->truncate( $reason, 255 ); 02435 02436 $logRelationsValues = array(); 02437 $logRelationsField = null; 02438 02439 if ( $id ) { // Protection of existing page 02440 if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) { 02441 return Status::newGood(); 02442 } 02443 02444 // Only certain restrictions can cascade... 02445 $editrestriction = isset( $limit['edit'] ) 02446 ? array( $limit['edit'] ) 02447 : $this->mTitle->getRestrictions( 'edit' ); 02448 foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) { 02449 $editrestriction[$key] = 'editprotected'; // backwards compatibility 02450 } 02451 foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) { 02452 $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility 02453 } 02454 02455 $cascadingRestrictionLevels = $wgCascadingRestrictionLevels; 02456 foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) { 02457 $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility 02458 } 02459 foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) { 02460 $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility 02461 } 02462 02463 // The schema allows multiple restrictions 02464 if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) { 02465 $cascade = false; 02466 } 02467 02468 // insert null revision to identify the page protection change as edit summary 02469 $latest = $this->getLatest(); 02470 $nullRevision = $this->insertProtectNullRevision( 02471 $revCommentMsg, 02472 $limit, 02473 $expiry, 02474 $cascade, 02475 $reason, 02476 $user 02477 ); 02478 02479 if ( $nullRevision === null ) { 02480 return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() ); 02481 } 02482 02483 $logRelationsField = 'pr_id'; 02484 02485 // Update restrictions table 02486 foreach ( $limit as $action => $restrictions ) { 02487 $dbw->delete( 02488 'page_restrictions', 02489 array( 02490 'pr_page' => $id, 02491 'pr_type' => $action 02492 ), 02493 __METHOD__ 02494 ); 02495 if ( $restrictions != '' ) { 02496 $dbw->insert( 02497 'page_restrictions', 02498 array( 02499 'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ), 02500 'pr_page' => $id, 02501 'pr_type' => $action, 02502 'pr_level' => $restrictions, 02503 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0, 02504 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] ) 02505 ), 02506 __METHOD__ 02507 ); 02508 $logRelationsValues[] = $dbw->insertId(); 02509 } 02510 } 02511 02512 // Clear out legacy restriction fields 02513 $dbw->update( 02514 'page', 02515 array( 'page_restrictions' => '' ), 02516 array( 'page_id' => $id ), 02517 __METHOD__ 02518 ); 02519 02520 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) ); 02521 wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) ); 02522 } else { // Protection of non-existing page (also known as "title protection") 02523 // Cascade protection is meaningless in this case 02524 $cascade = false; 02525 02526 if ( $limit['create'] != '' ) { 02527 $dbw->replace( 'protected_titles', 02528 array( array( 'pt_namespace', 'pt_title' ) ), 02529 array( 02530 'pt_namespace' => $this->mTitle->getNamespace(), 02531 'pt_title' => $this->mTitle->getDBkey(), 02532 'pt_create_perm' => $limit['create'], 02533 'pt_timestamp' => $dbw->timestamp(), 02534 'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ), 02535 'pt_user' => $user->getId(), 02536 'pt_reason' => $reason, 02537 ), __METHOD__ 02538 ); 02539 } else { 02540 $dbw->delete( 'protected_titles', 02541 array( 02542 'pt_namespace' => $this->mTitle->getNamespace(), 02543 'pt_title' => $this->mTitle->getDBkey() 02544 ), __METHOD__ 02545 ); 02546 } 02547 } 02548 02549 $this->mTitle->flushRestrictions(); 02550 InfoAction::invalidateCache( $this->mTitle ); 02551 02552 if ( $logAction == 'unprotect' ) { 02553 $params = array(); 02554 } else { 02555 $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry ); 02556 $params = array( $protectDescriptionLog, $cascade ? 'cascade' : '' ); 02557 } 02558 02559 // Update the protection log 02560 $log = new LogPage( 'protect' ); 02561 $logId = $log->addEntry( $logAction, $this->mTitle, $reason, $params, $user ); 02562 if ( $logRelationsField !== null && count( $logRelationsValues ) ) { 02563 $log->addRelations( $logRelationsField, $logRelationsValues, $logId ); 02564 } 02565 02566 return Status::newGood(); 02567 } 02568 02580 public function insertProtectNullRevision( $revCommentMsg, array $limit, 02581 array $expiry, $cascade, $reason, $user = null 02582 ) { 02583 global $wgContLang; 02584 $dbw = wfGetDB( DB_MASTER ); 02585 02586 // Prepare a null revision to be added to the history 02587 $editComment = $wgContLang->ucfirst( 02588 wfMessage( 02589 $revCommentMsg, 02590 $this->mTitle->getPrefixedText() 02591 )->inContentLanguage()->text() 02592 ); 02593 if ( $reason ) { 02594 $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; 02595 } 02596 $protectDescription = $this->protectDescription( $limit, $expiry ); 02597 if ( $protectDescription ) { 02598 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02599 $editComment .= wfMessage( 'parentheses' )->params( $protectDescription ) 02600 ->inContentLanguage()->text(); 02601 } 02602 if ( $cascade ) { 02603 $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02604 $editComment .= wfMessage( 'brackets' )->params( 02605 wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text() 02606 )->inContentLanguage()->text(); 02607 } 02608 02609 $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user ); 02610 if ( $nullRev ) { 02611 $nullRev->insertOn( $dbw ); 02612 02613 // Update page record and touch page 02614 $oldLatest = $nullRev->getParentId(); 02615 $this->updateRevisionOn( $dbw, $nullRev, $oldLatest ); 02616 } 02617 02618 return $nullRev; 02619 } 02620 02625 protected function formatExpiry( $expiry ) { 02626 global $wgContLang; 02627 $dbr = wfGetDB( DB_SLAVE ); 02628 02629 $encodedExpiry = $dbr->encodeExpiry( $expiry ); 02630 if ( $encodedExpiry != 'infinity' ) { 02631 return wfMessage( 02632 'protect-expiring', 02633 $wgContLang->timeanddate( $expiry, false, false ), 02634 $wgContLang->date( $expiry, false, false ), 02635 $wgContLang->time( $expiry, false, false ) 02636 )->inContentLanguage()->text(); 02637 } else { 02638 return wfMessage( 'protect-expiry-indefinite' ) 02639 ->inContentLanguage()->text(); 02640 } 02641 } 02642 02650 public function protectDescription( array $limit, array $expiry ) { 02651 $protectDescription = ''; 02652 02653 foreach ( array_filter( $limit ) as $action => $restrictions ) { 02654 # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ). 02655 # All possible message keys are listed here for easier grepping: 02656 # * restriction-create 02657 # * restriction-edit 02658 # * restriction-move 02659 # * restriction-upload 02660 $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text(); 02661 # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ), 02662 # with '' filtered out. All possible message keys are listed below: 02663 # * protect-level-autoconfirmed 02664 # * protect-level-sysop 02665 $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text(); 02666 02667 $expiryText = $this->formatExpiry( $expiry[$action] ); 02668 02669 if ( $protectDescription !== '' ) { 02670 $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text(); 02671 } 02672 $protectDescription .= wfMessage( 'protect-summary-desc' ) 02673 ->params( $actionText, $restrictionsText, $expiryText ) 02674 ->inContentLanguage()->text(); 02675 } 02676 02677 return $protectDescription; 02678 } 02679 02691 public function protectDescriptionLog( array $limit, array $expiry ) { 02692 global $wgContLang; 02693 02694 $protectDescriptionLog = ''; 02695 02696 foreach ( array_filter( $limit ) as $action => $restrictions ) { 02697 $expiryText = $this->formatExpiry( $expiry[$action] ); 02698 $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)"; 02699 } 02700 02701 return trim( $protectDescriptionLog ); 02702 } 02703 02713 protected static function flattenRestrictions( $limit ) { 02714 if ( !is_array( $limit ) ) { 02715 throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' ); 02716 } 02717 02718 $bits = array(); 02719 ksort( $limit ); 02720 02721 foreach ( array_filter( $limit ) as $action => $restrictions ) { 02722 $bits[] = "$action=$restrictions"; 02723 } 02724 02725 return implode( ':', $bits ); 02726 } 02727 02744 public function doDeleteArticle( 02745 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02746 ) { 02747 $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user ); 02748 return $status->isGood(); 02749 } 02750 02768 public function doDeleteArticleReal( 02769 $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null 02770 ) { 02771 global $wgUser, $wgContentHandlerUseDB; 02772 02773 wfDebug( __METHOD__ . "\n" ); 02774 02775 $status = Status::newGood(); 02776 02777 if ( $this->mTitle->getDBkey() === '' ) { 02778 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02779 return $status; 02780 } 02781 02782 $user = is_null( $user ) ? $wgUser : $user; 02783 if ( !wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) { 02784 if ( $status->isOK() ) { 02785 // Hook aborted but didn't set a fatal status 02786 $status->fatal( 'delete-hook-aborted' ); 02787 } 02788 return $status; 02789 } 02790 02791 $dbw = wfGetDB( DB_MASTER ); 02792 $dbw->begin( __METHOD__ ); 02793 02794 if ( $id == 0 ) { 02795 $this->loadPageData( 'forupdate' ); 02796 $id = $this->getID(); 02797 if ( $id == 0 ) { 02798 $dbw->rollback( __METHOD__ ); 02799 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02800 return $status; 02801 } 02802 } 02803 02804 // we need to remember the old content so we can use it to generate all deletion updates. 02805 $content = $this->getContent( Revision::RAW ); 02806 02807 // Bitfields to further suppress the content 02808 if ( $suppress ) { 02809 $bitfield = 0; 02810 // This should be 15... 02811 $bitfield |= Revision::DELETED_TEXT; 02812 $bitfield |= Revision::DELETED_COMMENT; 02813 $bitfield |= Revision::DELETED_USER; 02814 $bitfield |= Revision::DELETED_RESTRICTED; 02815 } else { 02816 $bitfield = 'rev_deleted'; 02817 } 02818 02819 // For now, shunt the revision data into the archive table. 02820 // Text is *not* removed from the text table; bulk storage 02821 // is left intact to avoid breaking block-compression or 02822 // immutable storage schemes. 02823 // 02824 // For backwards compatibility, note that some older archive 02825 // table entries will have ar_text and ar_flags fields still. 02826 // 02827 // In the future, we may keep revisions and mark them with 02828 // the rev_deleted field, which is reserved for this purpose. 02829 02830 $row = array( 02831 'ar_namespace' => 'page_namespace', 02832 'ar_title' => 'page_title', 02833 'ar_comment' => 'rev_comment', 02834 'ar_user' => 'rev_user', 02835 'ar_user_text' => 'rev_user_text', 02836 'ar_timestamp' => 'rev_timestamp', 02837 'ar_minor_edit' => 'rev_minor_edit', 02838 'ar_rev_id' => 'rev_id', 02839 'ar_parent_id' => 'rev_parent_id', 02840 'ar_text_id' => 'rev_text_id', 02841 'ar_text' => '\'\'', // Be explicit to appease 02842 'ar_flags' => '\'\'', // MySQL's "strict mode"... 02843 'ar_len' => 'rev_len', 02844 'ar_page_id' => 'page_id', 02845 'ar_deleted' => $bitfield, 02846 'ar_sha1' => 'rev_sha1', 02847 ); 02848 02849 if ( $wgContentHandlerUseDB ) { 02850 $row['ar_content_model'] = 'rev_content_model'; 02851 $row['ar_content_format'] = 'rev_content_format'; 02852 } 02853 02854 $dbw->insertSelect( 'archive', array( 'page', 'revision' ), 02855 $row, 02856 array( 02857 'page_id' => $id, 02858 'page_id = rev_page' 02859 ), __METHOD__ 02860 ); 02861 02862 // Now that it's safely backed up, delete it 02863 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); 02864 $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy 02865 02866 if ( !$ok ) { 02867 $dbw->rollback( __METHOD__ ); 02868 $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) ); 02869 return $status; 02870 } 02871 02872 if ( !$dbw->cascadingDeletes() ) { 02873 $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); 02874 } 02875 02876 // Clone the title, so we have the information we need when we log 02877 $logTitle = clone $this->mTitle; 02878 02879 // Log the deletion, if the page was suppressed, log it at Oversight instead 02880 $logtype = $suppress ? 'suppress' : 'delete'; 02881 02882 $logEntry = new ManualLogEntry( $logtype, 'delete' ); 02883 $logEntry->setPerformer( $user ); 02884 $logEntry->setTarget( $logTitle ); 02885 $logEntry->setComment( $reason ); 02886 $logid = $logEntry->insert(); 02887 02888 $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) { 02889 // Bug 56776: avoid deadlocks (especially from FileDeleteForm) 02890 $logEntry->publish( $logid ); 02891 } ); 02892 02893 if ( $commit ) { 02894 $dbw->commit( __METHOD__ ); 02895 } 02896 02897 $this->doDeleteUpdates( $id, $content ); 02898 02899 wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); 02900 $status->value = $logid; 02901 return $status; 02902 } 02903 02912 public function doDeleteUpdates( $id, Content $content = null ) { 02913 // update site status 02914 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) ); 02915 02916 // remove secondary indexes, etc 02917 $updates = $this->getDeletionUpdates( $content ); 02918 DataUpdate::runUpdates( $updates ); 02919 02920 // Reparse any pages transcluding this page 02921 LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' ); 02922 02923 // Reparse any pages including this image 02924 if ( $this->mTitle->getNamespace() == NS_FILE ) { 02925 LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' ); 02926 } 02927 02928 // Clear caches 02929 WikiPage::onArticleDelete( $this->mTitle ); 02930 02931 // Reset this object and the Title object 02932 $this->loadFromRow( false, self::READ_LATEST ); 02933 02934 // Search engine 02935 DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) ); 02936 } 02937 02962 public function doRollback( 02963 $fromP, $summary, $token, $bot, &$resultDetails, User $user 02964 ) { 02965 $resultDetails = null; 02966 02967 // Check permissions 02968 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user ); 02969 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user ); 02970 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) ); 02971 02972 if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) { 02973 $errors[] = array( 'sessionfailure' ); 02974 } 02975 02976 if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) { 02977 $errors[] = array( 'actionthrottledtext' ); 02978 } 02979 02980 // If there were errors, bail out now 02981 if ( !empty( $errors ) ) { 02982 return $errors; 02983 } 02984 02985 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user ); 02986 } 02987 03004 public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) { 03005 global $wgUseRCPatrol, $wgContLang; 03006 03007 $dbw = wfGetDB( DB_MASTER ); 03008 03009 if ( wfReadOnly() ) { 03010 return array( array( 'readonlytext' ) ); 03011 } 03012 03013 // Get the last editor 03014 $current = $this->getRevision(); 03015 if ( is_null( $current ) ) { 03016 // Something wrong... no page? 03017 return array( array( 'notanarticle' ) ); 03018 } 03019 03020 $from = str_replace( '_', ' ', $fromP ); 03021 // User name given should match up with the top revision. 03022 // If the user was deleted then $from should be empty. 03023 if ( $from != $current->getUserText() ) { 03024 $resultDetails = array( 'current' => $current ); 03025 return array( array( 'alreadyrolled', 03026 htmlspecialchars( $this->mTitle->getPrefixedText() ), 03027 htmlspecialchars( $fromP ), 03028 htmlspecialchars( $current->getUserText() ) 03029 ) ); 03030 } 03031 03032 // Get the last edit not by this guy... 03033 // Note: these may not be public values 03034 $user = intval( $current->getRawUser() ); 03035 $user_text = $dbw->addQuotes( $current->getRawUserText() ); 03036 $s = $dbw->selectRow( 'revision', 03037 array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), 03038 array( 'rev_page' => $current->getPage(), 03039 "rev_user != {$user} OR rev_user_text != {$user_text}" 03040 ), __METHOD__, 03041 array( 'USE INDEX' => 'page_timestamp', 03042 'ORDER BY' => 'rev_timestamp DESC' ) 03043 ); 03044 if ( $s === false ) { 03045 // No one else ever edited this page 03046 return array( array( 'cantrollback' ) ); 03047 } elseif ( $s->rev_deleted & Revision::DELETED_TEXT 03048 || $s->rev_deleted & Revision::DELETED_USER 03049 ) { 03050 // Only admins can see this text 03051 return array( array( 'notvisiblerev' ) ); 03052 } 03053 03054 // Set patrolling and bot flag on the edits, which gets rollbacked. 03055 // This is done before the rollback edit to have patrolling also on failure (bug 62157). 03056 $set = array(); 03057 if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { 03058 // Mark all reverted edits as bot 03059 $set['rc_bot'] = 1; 03060 } 03061 03062 if ( $wgUseRCPatrol ) { 03063 // Mark all reverted edits as patrolled 03064 $set['rc_patrolled'] = 1; 03065 } 03066 03067 if ( count( $set ) ) { 03068 $dbw->update( 'recentchanges', $set, 03069 array( /* WHERE */ 03070 'rc_cur_id' => $current->getPage(), 03071 'rc_user_text' => $current->getUserText(), 03072 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), 03073 ), __METHOD__ 03074 ); 03075 } 03076 03077 // Generate the edit summary if necessary 03078 $target = Revision::newFromId( $s->rev_id ); 03079 if ( empty( $summary ) ) { 03080 if ( $from == '' ) { // no public user name 03081 $summary = wfMessage( 'revertpage-nouser' ); 03082 } else { 03083 $summary = wfMessage( 'revertpage' ); 03084 } 03085 } 03086 03087 // Allow the custom summary to use the same args as the default message 03088 $args = array( 03089 $target->getUserText(), $from, $s->rev_id, 03090 $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ), 03091 $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() ) 03092 ); 03093 if ( $summary instanceof Message ) { 03094 $summary = $summary->params( $args )->inContentLanguage()->text(); 03095 } else { 03096 $summary = wfMsgReplaceArgs( $summary, $args ); 03097 } 03098 03099 // Trim spaces on user supplied text 03100 $summary = trim( $summary ); 03101 03102 // Truncate for whole multibyte characters. 03103 $summary = $wgContLang->truncate( $summary, 255 ); 03104 03105 // Save 03106 $flags = EDIT_UPDATE; 03107 03108 if ( $guser->isAllowed( 'minoredit' ) ) { 03109 $flags |= EDIT_MINOR; 03110 } 03111 03112 if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) { 03113 $flags |= EDIT_FORCE_BOT; 03114 } 03115 03116 // Actually store the edit 03117 $status = $this->doEditContent( 03118 $target->getContent(), 03119 $summary, 03120 $flags, 03121 $target->getId(), 03122 $guser 03123 ); 03124 03125 if ( !$status->isOK() ) { 03126 return $status->getErrorsArray(); 03127 } 03128 03129 // raise error, when the edit is an edit without a new version 03130 if ( empty( $status->value['revision'] ) ) { 03131 $resultDetails = array( 'current' => $current ); 03132 return array( array( 'alreadyrolled', 03133 htmlspecialchars( $this->mTitle->getPrefixedText() ), 03134 htmlspecialchars( $fromP ), 03135 htmlspecialchars( $current->getUserText() ) 03136 ) ); 03137 } 03138 03139 $revId = $status->value['revision']->getId(); 03140 03141 wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) ); 03142 03143 $resultDetails = array( 03144 'summary' => $summary, 03145 'current' => $current, 03146 'target' => $target, 03147 'newid' => $revId 03148 ); 03149 03150 return array(); 03151 } 03152 03164 public static function onArticleCreate( $title ) { 03165 // Update existence markers on article/talk tabs... 03166 if ( $title->isTalkPage() ) { 03167 $other = $title->getSubjectPage(); 03168 } else { 03169 $other = $title->getTalkPage(); 03170 } 03171 03172 $other->invalidateCache(); 03173 $other->purgeSquid(); 03174 03175 $title->touchLinks(); 03176 $title->purgeSquid(); 03177 $title->deleteTitleProtection(); 03178 } 03179 03185 public static function onArticleDelete( $title ) { 03186 // Update existence markers on article/talk tabs... 03187 if ( $title->isTalkPage() ) { 03188 $other = $title->getSubjectPage(); 03189 } else { 03190 $other = $title->getTalkPage(); 03191 } 03192 03193 $other->invalidateCache(); 03194 $other->purgeSquid(); 03195 03196 $title->touchLinks(); 03197 $title->purgeSquid(); 03198 03199 // File cache 03200 HTMLFileCache::clearFileCache( $title ); 03201 InfoAction::invalidateCache( $title ); 03202 03203 // Messages 03204 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 03205 MessageCache::singleton()->replace( $title->getDBkey(), false ); 03206 } 03207 03208 // Images 03209 if ( $title->getNamespace() == NS_FILE ) { 03210 $update = new HTMLCacheUpdate( $title, 'imagelinks' ); 03211 $update->doUpdate(); 03212 } 03213 03214 // User talk pages 03215 if ( $title->getNamespace() == NS_USER_TALK ) { 03216 $user = User::newFromName( $title->getText(), false ); 03217 if ( $user ) { 03218 $user->setNewtalk( false ); 03219 } 03220 } 03221 03222 // Image redirects 03223 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); 03224 } 03225 03233 public static function onArticleEdit( $title ) { 03234 // Invalidate caches of articles which include this page 03235 DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); 03236 03237 // Invalidate the caches of all pages which redirect here 03238 DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); 03239 03240 // Purge squid for this page only 03241 $title->purgeSquid(); 03242 03243 // Clear file cache for this page only 03244 HTMLFileCache::clearFileCache( $title ); 03245 InfoAction::invalidateCache( $title ); 03246 } 03247 03256 public function getCategories() { 03257 $id = $this->getId(); 03258 if ( $id == 0 ) { 03259 return TitleArray::newFromResult( new FakeResultWrapper( array() ) ); 03260 } 03261 03262 $dbr = wfGetDB( DB_SLAVE ); 03263 $res = $dbr->select( 'categorylinks', 03264 array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ), 03265 // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes 03266 // as not being aliases, and NS_CATEGORY is numeric 03267 array( 'cl_from' => $id ), 03268 __METHOD__ ); 03269 03270 return TitleArray::newFromResult( $res ); 03271 } 03272 03279 public function getHiddenCategories() { 03280 $result = array(); 03281 $id = $this->getId(); 03282 03283 if ( $id == 0 ) { 03284 return array(); 03285 } 03286 03287 $dbr = wfGetDB( DB_SLAVE ); 03288 $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ), 03289 array( 'cl_to' ), 03290 array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat', 03291 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ), 03292 __METHOD__ ); 03293 03294 if ( $res !== false ) { 03295 foreach ( $res as $row ) { 03296 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); 03297 } 03298 } 03299 03300 return $result; 03301 } 03302 03312 public static function getAutosummary( $oldtext, $newtext, $flags ) { 03313 // NOTE: stub for backwards-compatibility. assumes the given text is 03314 // wikitext. will break horribly if it isn't. 03315 03316 ContentHandler::deprecated( __METHOD__, '1.21' ); 03317 03318 $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); 03319 $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext ); 03320 $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext ); 03321 03322 return $handler->getAutosummary( $oldContent, $newContent, $flags ); 03323 } 03324 03332 public function getAutoDeleteReason( &$hasHistory ) { 03333 return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory ); 03334 } 03335 03343 public function updateCategoryCounts( array $added, array $deleted ) { 03344 $that = $this; 03345 $method = __METHOD__; 03346 $dbw = wfGetDB( DB_MASTER ); 03347 03348 // Do this at the end of the commit to reduce lock wait timeouts 03349 $dbw->onTransactionPreCommitOrIdle( 03350 function () use ( $dbw, $that, $method, $added, $deleted ) { 03351 $ns = $that->getTitle()->getNamespace(); 03352 03353 $addFields = array( 'cat_pages = cat_pages + 1' ); 03354 $removeFields = array( 'cat_pages = cat_pages - 1' ); 03355 if ( $ns == NS_CATEGORY ) { 03356 $addFields[] = 'cat_subcats = cat_subcats + 1'; 03357 $removeFields[] = 'cat_subcats = cat_subcats - 1'; 03358 } elseif ( $ns == NS_FILE ) { 03359 $addFields[] = 'cat_files = cat_files + 1'; 03360 $removeFields[] = 'cat_files = cat_files - 1'; 03361 } 03362 03363 if ( count( $added ) ) { 03364 $insertRows = array(); 03365 foreach ( $added as $cat ) { 03366 $insertRows[] = array( 03367 'cat_title' => $cat, 03368 'cat_pages' => 1, 03369 'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0, 03370 'cat_files' => ( $ns == NS_FILE ) ? 1 : 0, 03371 ); 03372 } 03373 $dbw->upsert( 03374 'category', 03375 $insertRows, 03376 array( 'cat_title' ), 03377 $addFields, 03378 $method 03379 ); 03380 } 03381 03382 if ( count( $deleted ) ) { 03383 $dbw->update( 03384 'category', 03385 $removeFields, 03386 array( 'cat_title' => $deleted ), 03387 $method 03388 ); 03389 } 03390 03391 foreach ( $added as $catName ) { 03392 $cat = Category::newFromName( $catName ); 03393 wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $that ) ); 03394 } 03395 03396 foreach ( $deleted as $catName ) { 03397 $cat = Category::newFromName( $catName ); 03398 wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $that ) ); 03399 } 03400 } 03401 ); 03402 } 03403 03409 public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) { 03410 if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { 03411 return; 03412 } 03413 03414 // templatelinks or imagelinks tables may have become out of sync, 03415 // especially if using variable-based transclusions. 03416 // For paranoia, check if things have changed and if 03417 // so apply updates to the database. This will ensure 03418 // that cascaded protections apply as soon as the changes 03419 // are visible. 03420 03421 // Get templates from templatelinks and images from imagelinks 03422 $id = $this->getId(); 03423 03424 $dbLinks = array(); 03425 03426 $dbr = wfGetDB( DB_SLAVE ); 03427 $res = $dbr->select( array( 'templatelinks' ), 03428 array( 'tl_namespace', 'tl_title' ), 03429 array( 'tl_from' => $id ), 03430 __METHOD__ 03431 ); 03432 03433 foreach ( $res as $row ) { 03434 $dbLinks["{$row->tl_namespace}:{$row->tl_title}"] = true; 03435 } 03436 03437 $dbr = wfGetDB( DB_SLAVE ); 03438 $res = $dbr->select( array( 'imagelinks' ), 03439 array( 'il_to' ), 03440 array( 'il_from' => $id ), 03441 __METHOD__ 03442 ); 03443 03444 foreach ( $res as $row ) { 03445 $dbLinks[NS_FILE . ":{$row->il_to}"] = true; 03446 } 03447 03448 // Get templates and images from parser output. 03449 $poLinks = array(); 03450 foreach ( $parserOutput->getTemplates() as $ns => $templates ) { 03451 foreach ( $templates as $dbk => $id ) { 03452 $poLinks["$ns:$dbk"] = true; 03453 } 03454 } 03455 foreach ( $parserOutput->getImages() as $dbk => $id ) { 03456 $poLinks[NS_FILE . ":$dbk"] = true; 03457 } 03458 03459 // Get the diff 03460 $links_diff = array_diff_key( $poLinks, $dbLinks ); 03461 03462 if ( count( $links_diff ) > 0 ) { 03463 // Whee, link updates time. 03464 // Note: we are only interested in links here. We don't need to get 03465 // other DataUpdate items from the parser output. 03466 $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); 03467 $u->doUpdate(); 03468 } 03469 } 03470 03478 public function getUsedTemplates() { 03479 return $this->mTitle->getTemplateLinksFrom(); 03480 } 03481 03494 public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) { 03495 global $wgParser, $wgUser; 03496 03497 wfDeprecated( __METHOD__, '1.19' ); 03498 03499 $user = is_null( $user ) ? $wgUser : $user; 03500 03501 if ( $popts === null ) { 03502 $popts = ParserOptions::newFromUser( $user ); 03503 } 03504 03505 return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts ); 03506 } 03507 03519 public function updateRestrictions( 03520 $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null 03521 ) { 03522 global $wgUser; 03523 03524 $user = is_null( $user ) ? $wgUser : $user; 03525 03526 return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK(); 03527 } 03528 03538 public function getDeletionUpdates( Content $content = null ) { 03539 if ( !$content ) { 03540 // load content object, which may be used to determine the necessary updates 03541 // XXX: the content may not be needed to determine the updates, then this would be overhead. 03542 $content = $this->getContent( Revision::RAW ); 03543 } 03544 03545 if ( !$content ) { 03546 $updates = array(); 03547 } else { 03548 $updates = $content->getDeletionUpdates( $this ); 03549 } 03550 03551 wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) ); 03552 return $updates; 03553 } 03554 }