X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Fpage%2FWikiPage.php;h=544d23d290e542c3b95302c9a36a4683c11e33ba;hp=27a8283af482bf1df524e681f5ea9457a8f2a74e;hb=3f59cb9f3a53ad28f8a95fe299c5de6abd24b453;hpb=d9942a0f28d9c8bdab49b502e4655706f5969f8c diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 27a8283af4..544d23d290 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -20,8 +20,14 @@ * @file */ -use \MediaWiki\Logger\LoggerFactory; -use \MediaWiki\MediaWikiServices; +use MediaWiki\Edit\PreparedEdit; +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; +use Wikimedia\Assert\Assert; +use Wikimedia\Rdbms\FakeResultWrapper; +use Wikimedia\Rdbms\IDatabase; +use Wikimedia\Rdbms\DBError; +use Wikimedia\Rdbms\DBUnexpectedError; /** * Class representing a MediaWiki article and history. @@ -45,7 +51,7 @@ class WikiPage implements Page, IDBAccessObject { public $mLatest = false; // !< Integer (false means "not loaded") /**@}}*/ - /** @var stdClass Map of cache fields (text, parser output, ect) for a proposed/new edit */ + /** @var PreparedEdit Map of cache fields (text, parser output, ect) for a proposed/new edit */ public $mPreparedEdit = false; /** @@ -83,11 +89,6 @@ class WikiPage implements Page, IDBAccessObject { */ protected $mLinksUpdated = '19700101000000'; - const PURGE_CDN_CACHE = 1; // purge CDN cache for page variant URLs - const PURGE_CLUSTER_PCACHE = 2; // purge parser cache in the local datacenter - const PURGE_GLOBAL_PCACHE = 4; // set page_touched to clear parser cache in all datacenters - const PURGE_ALL = 7; - /** * Constructor and clear the article * @param Title $title Reference to a Title object. @@ -151,15 +152,18 @@ class WikiPage implements Page, IDBAccessObject { * @return WikiPage|null */ public static function newFromID( $id, $from = 'fromdb' ) { - // page id's are never 0 or negative, see bug 61166 + // page ids are never 0 or negative, see T63166 if ( $id < 1 ) { return null; } $from = self::convertSelectType( $from ); $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA ); + $pageQuery = self::getQueryInfo(); $row = $db->selectRow( - 'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ ); + $pageQuery['tables'], $pageQuery['fields'], [ 'page_id' => $id ], __METHOD__, + [], $pageQuery['joins'] + ); if ( !$row ) { return null; } @@ -191,15 +195,15 @@ class WikiPage implements Page, IDBAccessObject { */ private static function convertSelectType( $type ) { switch ( $type ) { - case 'fromdb': - return self::READ_NORMAL; - case 'fromdbmaster': - return self::READ_LATEST; - case 'forupdate': - return self::READ_LOCKING; - default: - // It may already be an integer or whatever else - return $type; + case 'fromdb': + return self::READ_NORMAL; + case 'fromdbmaster': + return self::READ_LATEST; + case 'forupdate': + return self::READ_LOCKING; + default: + // It may already be an integer or whatever else + return $type; } } @@ -207,6 +211,7 @@ class WikiPage implements Page, IDBAccessObject { * @todo Move this UI stuff somewhere else * * @see ContentHandler::getActionOverrides + * @return array */ public function getActionOverrides() { return $this->getContentHandler()->getActionOverrides(); @@ -257,7 +262,7 @@ class WikiPage implements Page, IDBAccessObject { $this->mTimestamp = ''; $this->mIsRedirect = false; $this->mLatest = false; - // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks + // T59026: do not clear mPreparedEdit since prepareTextForEdit() already checks // the requested rev ID and content against the cached one for equality. For most // content types, the output should not change during the lifetime of this cache. // Clearing it can cause extra parses on edit for no reason. @@ -276,11 +281,14 @@ class WikiPage implements Page, IDBAccessObject { * Return the list of revision fields that should be selected to create * a new page. * + * @deprecated since 1.31, use self::getQueryInfo() instead. * @return array */ public static function selectFields() { global $wgContentHandlerUseDB, $wgPageLanguageUseDB; + wfDeprecated( __METHOD__, '1.31' ); + $fields = [ 'page_id', 'page_namespace', @@ -306,6 +314,47 @@ class WikiPage implements Page, IDBAccessObject { return $fields; } + /** + * Return the tables, fields, and join conditions to be selected to create + * a new page object. + * @since 1.31 + * @return array With three keys: + * - tables: (string[]) to include in the `$table` to `IDatabase->select()` + * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` + * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` + */ + public static function getQueryInfo() { + global $wgContentHandlerUseDB, $wgPageLanguageUseDB; + + $ret = [ + 'tables' => [ 'page' ], + 'fields' => [ + 'page_id', + 'page_namespace', + 'page_title', + 'page_restrictions', + 'page_is_redirect', + 'page_is_new', + 'page_random', + 'page_touched', + 'page_links_updated', + 'page_latest', + 'page_len', + ], + 'joins' => [], + ]; + + if ( $wgContentHandlerUseDB ) { + $ret['fields'][] = 'page_content_model'; + } + + if ( $wgPageLanguageUseDB ) { + $ret['fields'][] = 'page_lang'; + } + + return $ret; + } + /** * Fetch a page record with the given conditions * @param IDatabase $dbr @@ -314,14 +363,23 @@ class WikiPage implements Page, IDBAccessObject { * @return object|bool Database result resource, or false on failure */ protected function pageData( $dbr, $conditions, $options = [] ) { - $fields = self::selectFields(); + $pageQuery = self::getQueryInfo(); // Avoid PHP 7.1 warning of passing $this by reference $wikiPage = $this; - Hooks::run( 'ArticlePageDataBefore', [ &$wikiPage, &$fields ] ); + Hooks::run( 'ArticlePageDataBefore', [ + &$wikiPage, &$pageQuery['fields'], &$pageQuery['tables'], &$pageQuery['joins'] + ] ); - $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options ); + $row = $dbr->selectRow( + $pageQuery['tables'], + $pageQuery['fields'], + $conditions, + __METHOD__, + $options, + $pageQuery['joins'] + ); Hooks::run( 'ArticlePageDataAfter', [ &$wikiPage, &$row ] ); @@ -376,16 +434,19 @@ class WikiPage implements Page, IDBAccessObject { if ( is_int( $from ) ) { list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from ); - $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts ); + $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $db = $loadBalancer->getConnection( $index ); + $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts ); if ( !$data && $index == DB_REPLICA - && wfGetLB()->getServerCount() > 1 - && wfGetLB()->hasOrMadeRecentMasterChanges() + && $loadBalancer->getServerCount() > 1 + && $loadBalancer->hasOrMadeRecentMasterChanges() ) { $from = self::READ_LATEST; list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from ); - $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts ); + $db = $loadBalancer->getConnection( $index ); + $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts ); } } else { // No idea from where the caller got this data, assume replica DB. @@ -424,7 +485,7 @@ class WikiPage implements Page, IDBAccessObject { $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated ); $this->mIsRedirect = intval( $data->page_is_redirect ); $this->mLatest = intval( $data->page_latest ); - // Bug 37225: $latest may no longer match the cached latest Revision object. + // T39225: $latest may no longer match the cached latest Revision object. // Double-check the ID of any cached latest Revision object for consistency. if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) { $this->mLastRevision = null; @@ -504,7 +565,7 @@ class WikiPage implements Page, IDBAccessObject { $cache = ObjectCache::getMainWANInstance(); return $cache->getWithSetCallback( - $cache->makeKey( 'page', 'content-model', $this->getLatest() ), + $cache->makeKey( 'page-content-model', $this->getLatest() ), $cache::TTL_MONTH, function () { $rev = $this->getRevision(); @@ -573,35 +634,12 @@ class WikiPage implements Page, IDBAccessObject { * @return Revision|null */ public function getOldestRevision() { - // Try using the replica DB first, then try the master - $continue = 2; - $db = wfGetDB( DB_REPLICA ); - $revSelectFields = Revision::selectFields(); - - $row = null; - while ( $continue ) { - $row = $db->selectRow( - [ 'revision' ], - $revSelectFields, - [ - 'rev_page' => $this->getId() - ], - __METHOD__, - [ - 'ORDER BY' => 'rev_timestamp ASC' - ] - ); - - if ( $row ) { - $continue = 0; - } else { - $db = wfGetDB( DB_MASTER ); - $continue--; - } + $rev = $this->mTitle->getFirstRevision(); + if ( !$rev ) { + $rev = $this->mTitle->getFirstRevision( Title::GAID_FOR_UPDATE ); } - - return $row ? Revision::newFromRow( $row ) : null; + return $rev; } /** @@ -619,7 +657,7 @@ class WikiPage implements Page, IDBAccessObject { } if ( $this->mDataLoadedFrom == self::READ_LOCKING ) { - // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always + // T39225: if session S1 loads the page row FOR UPDATE, the result always // includes the latest changes committed. This is true even within REPEATABLE-READ // transactions, where S1 normally only sees changes committed before the first S1 // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it @@ -636,7 +674,7 @@ class WikiPage implements Page, IDBAccessObject { $revision = Revision::newFromPageId( $this->getId(), $latest, $flags ); } else { $dbr = wfGetDB( DB_REPLICA ); - $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest ); + $revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest ); } if ( $revision ) { // sanity @@ -799,7 +837,7 @@ class WikiPage implements Page, IDBAccessObject { * Determine whether a page would be suitable for being counted as an * article in the site_stats table based on the title & its content * - * @param object|bool $editInfo (false): object returned by prepareTextForEdit(), + * @param PreparedEdit|bool $editInfo (false): object returned by prepareTextForEdit(), * if false, the current database state will be used * @return bool */ @@ -894,11 +932,10 @@ class WikiPage implements Page, IDBAccessObject { } // Update the DB post-send if the page has not cached since now - $that = $this; $latest = $this->getLatest(); DeferredUpdates::addCallableUpdate( - function () use ( $that, $retval, $latest ) { - $that->insertRedirectEntry( $retval, $latest ); + function () use ( $retval, $latest ) { + $this->insertRedirectEntry( $retval, $latest ); }, DeferredUpdates::POSTSEND, wfGetDB( DB_MASTER ) @@ -999,18 +1036,12 @@ class WikiPage implements Page, IDBAccessObject { $dbr = wfGetDB( DB_REPLICA ); - if ( $dbr->implicitGroupby() ) { - $realNameField = 'user_real_name'; - } else { - $realNameField = 'MIN(user_real_name) AS user_real_name'; - } - $tables = [ 'revision', 'user' ]; $fields = [ 'user_id' => 'rev_user', 'user_name' => 'rev_user_text', - $realNameField, + 'user_real_name' => 'MIN(user_real_name)', 'timestamp' => 'MAX(rev_timestamp)', ]; @@ -1063,9 +1094,9 @@ class WikiPage implements Page, IDBAccessObject { * * @since 1.19 * @param ParserOptions $parserOptions ParserOptions to use for the parse operation - * @param null|int $oldid Revision ID to get the text from, passing null or 0 will - * get the current revision (default value) - * @param bool $forceParse Force reindexing, regardless of cache settings + * @param null|int $oldid Revision ID to get the text from, passing null or 0 will + * get the current revision (default value) + * @param bool $forceParse Force reindexing, regardless of cache settings * @return bool|ParserOutput ParserOutput or false if the revision was not found */ public function getParserOutput( @@ -1073,6 +1104,13 @@ class WikiPage implements Page, IDBAccessObject { ) { $useParserCache = ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid ); + + if ( $useParserCache && !$parserOptions->isSafeToCache() ) { + throw new InvalidArgumentException( + 'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.' + ); + } + wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); if ( $parserOptions->getStubThreshold() ) { @@ -1080,7 +1118,8 @@ class WikiPage implements Page, IDBAccessObject { } if ( $useParserCache ) { - $parserOutput = ParserCache::singleton()->get( $this, $parserOptions ); + $parserOutput = MediaWikiServices::getInstance()->getParserCache() + ->get( $this, $parserOptions ); if ( $parserOutput !== false ) { return $parserOutput; } @@ -1118,10 +1157,11 @@ class WikiPage implements Page, IDBAccessObject { /** * Perform the actions of a page purging - * @param integer $flags Bitfield of WikiPage::PURGE_* constants * @return bool + * @note In 1.28 (and only 1.28), this took a $flags parameter that + * controlled how much purging was done. */ - public function doPurge( $flags = self::PURGE_ALL ) { + public function doPurge() { // Avoid PHP 7.1 warning of passing $this by reference $wikiPage = $this; @@ -1129,30 +1169,15 @@ class WikiPage implements Page, IDBAccessObject { return false; } - if ( ( $flags & self::PURGE_GLOBAL_PCACHE ) == self::PURGE_GLOBAL_PCACHE ) { - // Set page_touched in the database to invalidate all DC caches - $this->mTitle->invalidateCache(); - } elseif ( ( $flags & self::PURGE_CLUSTER_PCACHE ) == self::PURGE_CLUSTER_PCACHE ) { - // Delete the parser options key in the local cluster to invalidate the DC cache - ParserCache::singleton()->deleteOptionsKey( $this ); - // Avoid sending HTTP 304s in ViewAction to the client who just issued the purge - $cache = ObjectCache::getLocalClusterInstance(); - $cache->set( - $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ), - wfTimestamp( TS_MW ), - $cache::TTL_HOUR - ); - } + $this->mTitle->invalidateCache(); - if ( ( $flags & self::PURGE_CDN_CACHE ) == self::PURGE_CDN_CACHE ) { - // Clear any HTML file cache - HTMLFileCache::clearFileCache( $this->getTitle() ); - // Send purge after any page_touched above update was committed - DeferredUpdates::addUpdate( - new CdnCacheUpdate( $this->mTitle->getCdnUrls() ), - DeferredUpdates::PRESEND - ); - } + // Clear file cache + HTMLFileCache::clearFileCache( $this->getTitle() ); + // Send purge after above page_touched update was committed + DeferredUpdates::addUpdate( + new CdnCacheUpdate( $this->mTitle->getCdnUrls() ), + DeferredUpdates::PRESEND + ); if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { $messageCache = MessageCache::singleton(); @@ -1162,18 +1187,6 @@ class WikiPage implements Page, IDBAccessObject { return true; } - /** - * Get the last time a user explicitly purged the page via action=purge - * - * @return string|bool TS_MW timestamp or false - * @since 1.28 - */ - public function getLastPurgeTimestamp() { - $cache = ObjectCache::getLocalClusterInstance(); - - return $cache->get( $cache->makeKey( 'page', 'last-dc-purge', $this->getId() ) ); - } - /** * Insert a new empty page record for this article. * This *must* be followed up by creating a revision @@ -1189,11 +1202,10 @@ class WikiPage implements Page, IDBAccessObject { * page ID is already in use. */ public function insertOn( $dbw, $pageId = null ) { - $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' ); + $pageIdForInsert = $pageId ? [ 'page_id' => $pageId ] : []; $dbw->insert( 'page', [ - 'page_id' => $pageIdForInsert, 'page_namespace' => $this->mTitle->getNamespace(), 'page_title' => $this->mTitle->getDBkey(), 'page_restrictions' => '', @@ -1203,13 +1215,13 @@ class WikiPage implements Page, IDBAccessObject { 'page_touched' => $dbw->timestamp(), 'page_latest' => 0, // Fill this in shortly... 'page_len' => 0, // Fill this in shortly... - ], + ] + $pageIdForInsert, __METHOD__, 'IGNORE' ); if ( $dbw->affectedRows() > 0 ) { - $newid = $pageId ?: $dbw->insertId(); + $newid = $pageId ? (int)$pageId : $dbw->insertId(); $this->mId = $newid; $this->mTitle->resetArticleID( $newid ); @@ -1255,8 +1267,11 @@ class WikiPage implements Page, IDBAccessObject { $conditions['page_latest'] = $lastRevision; } + $revId = $revision->getId(); + Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' ); + $row = [ /* SET */ - 'page_latest' => $revision->getId(), + 'page_latest' => $revId, 'page_touched' => $dbw->timestamp( $revision->getTimestamp() ), 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, 'page_is_redirect' => $rt !== null ? 1 : 0, @@ -1296,7 +1311,7 @@ class WikiPage implements Page, IDBAccessObject { * Add row to the redirect table if this is a redirect, remove otherwise. * * @param IDatabase $dbw - * @param Title $redirectTitle Title object pointing to the redirect target, + * @param Title|null $redirectTitle Title object pointing to the redirect target, * or NULL if this is not a redirect * @param null|bool $lastRevIsRedirect If given, will optimize adding and * removing rows in redirect table. @@ -1339,7 +1354,6 @@ class WikiPage implements Page, IDBAccessObject { * @return bool */ public function updateIfNewerOn( $dbw, $revision ) { - $row = $dbw->selectRow( [ 'revision', 'page' ], [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ], @@ -1411,19 +1425,19 @@ class WikiPage implements Page, IDBAccessObject { public function replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null ) { - $baseRevId = null; if ( $edittime && $sectionId !== 'new' ) { - $dbr = wfGetDB( DB_REPLICA ); + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $dbr = $lb->getConnection( DB_REPLICA ); $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime ); // Try the master if this thread may have just added it. // This could be abstracted into a Revision method, but we don't want // to encourage loading of revisions by timestamp. if ( !$rev - && wfGetLB()->getServerCount() > 1 - && wfGetLB()->hasOrMadeRecentMasterChanges() + && $lb->getServerCount() > 1 + && $lb->hasOrMadeRecentMasterChanges() ) { - $dbw = wfGetDB( DB_MASTER ); + $dbw = $lb->getConnection( DB_MASTER ); $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime ); } if ( $rev ) { @@ -1450,7 +1464,6 @@ class WikiPage implements Page, IDBAccessObject { public function replaceSectionAtRev( $sectionId, Content $sectionContent, $sectionTitle = '', $baseRevId = null ) { - if ( strval( $sectionId ) === '' ) { // Whole-page edit; let the whole text through $newContent = $sectionContent; @@ -1460,7 +1473,7 @@ class WikiPage implements Page, IDBAccessObject { $this->getContentHandler()->getModelID() ); } - // Bug 30711: always use current version when adding a new section + // T32711: always use current version when adding a new section if ( is_null( $baseRevId ) || $sectionId === 'new' ) { $oldContent = $this->getContent(); } else { @@ -1611,13 +1624,20 @@ class WikiPage implements Page, IDBAccessObject { $old_revision = $this->getRevision(); // current revision $old_content = $this->getContent( Revision::RAW ); // current revision's content - if ( $old_content && $old_content->getModel() !== $content->getModel() ) { - $tags[] = 'mw-contentmodelchange'; + $handler = $content->getContentHandler(); + $tag = $handler->getChangeTag( $old_content, $content, $flags ); + // If there is no applicable tag, null is returned, so we need to check + if ( $tag ) { + $tags[] = $tag; + } + + // Check for undo tag + if ( $undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) { + $tags[] = 'mw-undo'; } - // Provide autosummaries if one is not provided and autosummaries are enabled + // Provide autosummaries if summary is not provided and autosummaries are enabled if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) { - $handler = $content->getContentHandler(); $summary = $handler->getAutosummary( $old_content, $content, $flags ); } @@ -1634,7 +1654,7 @@ class WikiPage implements Page, IDBAccessObject { $meta = [ 'bot' => ( $flags & EDIT_FORCE_BOT ), 'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ), - 'serialized' => $editInfo->pst, + 'serialized' => $pstContent->serialize( $serialFormat ), 'serialFormat' => $serialFormat, 'baseRevId' => $baseRevId, 'oldRevision' => $old_revision, @@ -1664,7 +1684,7 @@ class WikiPage implements Page, IDBAccessObject { /** * @param Content $content Pre-save transform content - * @param integer $flags + * @param int $flags * @param User $user * @param string $summary * @param array $meta @@ -1685,7 +1705,7 @@ class WikiPage implements Page, IDBAccessObject { // Convenience variables $now = wfTimestampNow(); $oldid = $meta['oldId']; - /** @var $oldContent Content|null */ + /** @var Content|null $oldContent */ $oldContent = $meta['oldContent']; $newsize = $content->getSize(); @@ -1695,31 +1715,31 @@ class WikiPage implements Page, IDBAccessObject { return $status; } elseif ( !$oldContent ) { - // Sanity check for bug 37225 + // Sanity check for T39225 throw new MWException( "Could not find text for current revision {$oldid}." ); } - // @TODO: pass content object?! - $revision = new Revision( [ - 'page' => $this->getId(), - 'title' => $this->mTitle, // for determining the default content model - 'comment' => $summary, - 'minor_edit' => $meta['minor'], - 'text' => $meta['serialized'], - 'len' => $newsize, - 'parent_id' => $oldid, - 'user' => $user->getId(), - 'user_text' => $user->getName(), - 'timestamp' => $now, - 'content_model' => $content->getModel(), - 'content_format' => $meta['serialFormat'], - ] ); - $changed = !$content->equals( $oldContent ); $dbw = wfGetDB( DB_MASTER ); if ( $changed ) { + // @TODO: pass content object?! + $revision = new Revision( [ + 'page' => $this->getId(), + 'title' => $this->mTitle, // for determining the default content model + 'comment' => $summary, + 'minor_edit' => $meta['minor'], + 'text' => $meta['serialized'], + 'len' => $newsize, + 'parent_id' => $oldid, + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $meta['serialFormat'], + ] ); + $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user ); $status->merge( $prepStatus ); if ( !$status->isOK() ) { @@ -1784,13 +1804,11 @@ class WikiPage implements Page, IDBAccessObject { $dbw->endAtomic( __METHOD__ ); $this->mTimestamp = $now; } else { - // Bug 32948: revision ID must be set to page {{REVISIONID}} and + // T34948: revision ID must be set to page {{REVISIONID}} and // related variables correctly. Likewise for {{REVISIONUSER}} (T135261). - $revision->setId( $this->getLatest() ); - $revision->setUserIdAndName( - $this->getUser( Revision::RAW ), - $this->getUserText( Revision::RAW ) - ); + // Since we don't insert a new revision into the database, the least + // error-prone way is to reuse given old revision. + $revision = $meta['oldRevision']; } if ( $changed ) { @@ -1839,7 +1857,7 @@ class WikiPage implements Page, IDBAccessObject { /** * @param Content $content Pre-save transform content - * @param integer $flags + * @param int $flags * @param User $user * @param string $summary * @param array $meta @@ -1948,10 +1966,10 @@ class WikiPage implements Page, IDBAccessObject { $wikiPage = $this; // Trigger post-create hook $params = [ &$wikiPage, &$user, $content, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision ]; + $flags & EDIT_MINOR, null, null, &$flags, $revision ]; Hooks::run( 'PageContentInsertComplete', $params ); // Trigger post-save hook - $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] ); + $params = array_merge( $params, [ &$status, $meta['baseRevId'], 0 ] ); Hooks::run( 'PageContentSaveComplete', $params ); } ), @@ -1989,7 +2007,9 @@ class WikiPage implements Page, IDBAccessObject { /** * Prepare content which is about to be saved. - * Returns a stdClass with source, pst and output members + * + * Prior to 1.30, this returned a stdClass object with the same class + * members. * * @param Content $content * @param Revision|int|null $revision Revision object. For backwards compatibility, a @@ -1998,7 +2018,7 @@ class WikiPage implements Page, IDBAccessObject { * @param string|null $serialFormat * @param bool $useCache Check shared prepared edit cache * - * @return object + * @return PreparedEdit * * @since 1.21 */ @@ -2015,6 +2035,7 @@ class WikiPage implements Page, IDBAccessObject { // This code path is deprecated, and nothing is known to // use it, so performance here shouldn't be a worry. if ( $revid !== null ) { + wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' ); $revision = Revision::newFromId( $revid, Revision::READ_LATEST ); } else { $revision = null; @@ -2024,7 +2045,7 @@ class WikiPage implements Page, IDBAccessObject { $user = is_null( $user ) ? $wgUser : $user; // XXX: check $user->getId() here??? - // Use a sane default for $serialFormat, see bug 57026 + // Use a sane default for $serialFormat, see T59026 if ( $serialFormat === null ) { $serialFormat = $content->getContentHandler()->getDefaultFormat(); } @@ -2048,7 +2069,7 @@ class WikiPage implements Page, IDBAccessObject { $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang ); Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] ); - $edit = (object)[]; + $edit = new PreparedEdit(); if ( $cachedEdit ) { $edit->timestamp = $cachedEdit->timestamp; } else { @@ -2108,15 +2129,6 @@ class WikiPage implements Page, IDBAccessObject { $edit->newContent = $content; $edit->oldContent = $this->getContent( Revision::RAW ); - // NOTE: B/C for hooks! don't use these fields! - $edit->newText = $edit->newContent - ? ContentHandler::getContentText( $edit->newContent ) - : ''; - $edit->oldText = $edit->oldContent - ? ContentHandler::getContentText( $edit->oldContent ) - : ''; - $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : ''; - if ( $edit->output ) { $edit->output->setCacheTime( wfTimestampNow() ); } @@ -2136,13 +2148,13 @@ class WikiPage implements Page, IDBAccessObject { * @param Revision $revision * @param User $user User object that did the revision * @param array $options Array of options, following indexes are used: - * - changed: boolean, whether the revision changed the content (default true) - * - created: boolean, whether the revision created the page (default false) - * - moved: boolean, whether the page was moved (default false) - * - restored: boolean, whether the page was undeleted (default false) + * - changed: bool, whether the revision changed the content (default true) + * - created: bool, whether the revision created the page (default false) + * - moved: bool, whether the page was moved (default false) + * - restored: bool, whether the page was undeleted (default false) * - oldrevision: Revision object for the pre-update revision (default null) - * - oldcountable: boolean, null, or string 'no-change' (default null): - * - boolean: whether the page was counted as an article before that + * - oldcountable: bool, null, or string 'no-change' (default null): + * - bool: whether the page was counted as an article before that * revision, only used in changed is true and created is false * - null: if created is false, don't update the article count; if created * is true, do update the article count @@ -2189,18 +2201,19 @@ class WikiPage implements Page, IDBAccessObject { // Save it to the parser cache. // Make sure the cache time matches page_touched to avoid double parsing. - ParserCache::singleton()->save( + MediaWikiServices::getInstance()->getParserCache()->save( $editInfo->output, $this, $editInfo->popts, $revision->getTimestamp(), $editInfo->revid ); // Update the links tables and other secondary data if ( $content ) { - $recursive = $options['changed']; // bug 50785 + $recursive = $options['changed']; // T52785 $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, $recursive, $editInfo->output ); foreach ( $updates as $update ) { + $update->setCause( 'edit-page', $user->getName() ); if ( $update instanceof LinksUpdate ) { $update->setRevision( $revision ); $update->setTriggeringUser( $user ); @@ -2257,9 +2270,11 @@ class WikiPage implements Page, IDBAccessObject { $good = 0; } $edits = $options['changed'] ? 1 : 0; - $total = $options['created'] ? 1 : 0; + $pages = $options['created'] ? 1 : 0; - DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) ); + DeferredUpdates::addUpdate( SiteStatsUpdate::factory( + [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ] + ) ); DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) ); // If this is another user's talk page, update newtalk. @@ -2298,7 +2313,7 @@ class WikiPage implements Page, IDBAccessObject { if ( $options['created'] ) { self::onArticleCreate( $this->mTitle ); - } elseif ( $options['changed'] ) { // bug 50785 + } elseif ( $options['changed'] ) { // T52785 self::onArticleEdit( $this->mTitle, $revision ); } @@ -2324,10 +2339,10 @@ class WikiPage implements Page, IDBAccessObject { public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user, $tags = null ) { - global $wgCascadingRestrictionLevels, $wgContLang; + global $wgCascadingRestrictionLevels; if ( wfReadOnly() ) { - return Status::newFatal( 'readonlytext', wfReadOnlyReason() ); + return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) ); } $this->loadPageData( 'fromdbmaster' ); @@ -2397,9 +2412,6 @@ class WikiPage implements Page, IDBAccessObject { $logAction = 'protect'; } - // Truncate for whole multibyte characters - $reason = $wgContLang->truncate( $reason, 255 ); - $logRelationsValues = []; $logRelationsField = null; $logParamsDetails = []; @@ -2471,7 +2483,6 @@ class WikiPage implements Page, IDBAccessObject { $dbw->insert( 'page_restrictions', [ - 'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ), 'pr_page' => $id, 'pr_type' => $action, 'pr_level' => $restrictions, @@ -2509,6 +2520,7 @@ class WikiPage implements Page, IDBAccessObject { $cascade = false; if ( $limit['create'] != '' ) { + $commentFields = CommentStore::getStore()->insert( $dbw, 'pt_reason', $reason ); $dbw->replace( 'protected_titles', [ [ 'pt_namespace', 'pt_title' ] ], [ @@ -2518,8 +2530,7 @@ class WikiPage implements Page, IDBAccessObject { 'pt_timestamp' => $dbw->timestamp(), 'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ), 'pt_user' => $user->getId(), - 'pt_reason' => $reason, - ], __METHOD__ + ] + $commentFields, __METHOD__ ); $logParamsDetails[] = [ 'type' => 'create', @@ -2763,6 +2774,7 @@ class WikiPage implements Page, IDBAccessObject { * @param array|string &$error Array of errors to append to * @param User $user The deleting user * @param array $tags Tags to apply to the deletion action + * @param string $logsubtype * @return Status Status object; if successful, $status->value is the log_id of the * deletion log entry. If the page couldn't be deleted because it wasn't * found, $status is a non-fatal 'cannotdelete' error @@ -2771,7 +2783,7 @@ class WikiPage implements Page, IDBAccessObject { $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null, $tags = [], $logsubtype = 'delete' ) { - global $wgUser, $wgContentHandlerUseDB; + global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage; wfDebug( __METHOD__ . "\n" ); @@ -2835,13 +2847,15 @@ class WikiPage implements Page, IDBAccessObject { $content = null; } - $fields = Revision::selectFields(); + $commentStore = CommentStore::getStore(); + + $revQuery = Revision::getQueryInfo(); $bitfield = false; // Bitfields to further suppress the content if ( $suppress ) { $bitfield = Revision::SUPPRESSED_ALL; - $fields = array_diff( $fields, [ 'rev_deleted' ] ); + $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] ); } // For now, shunt the revision data into the archive table. @@ -2853,19 +2867,26 @@ class WikiPage implements Page, IDBAccessObject { // Get all of the page revisions $res = $dbw->select( - 'revision', - $fields, + $revQuery['tables'], + $revQuery['fields'], [ 'rev_page' => $id ], __METHOD__, - 'FOR UPDATE' + 'FOR UPDATE', + $revQuery['joins'] ); + // Build their equivalent archive rows $rowsInsert = []; + $revids = []; + + /** @var int[] Revision IDs of edits that were made by IPs */ + $ipRevIds = []; + foreach ( $res as $row ) { + $comment = $commentStore->getComment( 'rev_comment', $row ); $rowInsert = [ 'ar_namespace' => $namespace, 'ar_title' => $dbKey, - 'ar_comment' => $row->rev_comment, 'ar_user' => $row->rev_user, 'ar_user_text' => $row->rev_user_text, 'ar_timestamp' => $row->rev_timestamp, @@ -2879,12 +2900,19 @@ class WikiPage implements Page, IDBAccessObject { 'ar_page_id' => $id, 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted, 'ar_sha1' => $row->rev_sha1, - ]; + ] + $commentStore->insert( $dbw, 'ar_comment', $comment ); if ( $wgContentHandlerUseDB ) { $rowInsert['ar_content_model'] = $row->rev_content_model; $rowInsert['ar_content_format'] = $row->rev_content_format; } $rowsInsert[] = $rowInsert; + $revids[] = $row->rev_id; + + // Keep track of IP edits, so that the corresponding rows can + // be deleted in the ip_changes table. + if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) { + $ipRevIds[] = $row->rev_id; + } } // Copy them into the archive table $dbw->insert( 'archive', $rowsInsert, __METHOD__ ); @@ -2899,6 +2927,14 @@ class WikiPage implements Page, IDBAccessObject { // Now that it's safely backed up, delete it $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ ); $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ ); + if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) { + $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ ); + } + + // Also delete records from ip_changes as applicable. + if ( count( $ipRevIds ) > 0 ) { + $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ ); + } // Log the deletion, if the page was suppressed, put it in the suppression log instead $logtype = $suppress ? 'suppress' : 'delete'; @@ -2912,7 +2948,7 @@ class WikiPage implements Page, IDBAccessObject { $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) { - // Bug 56776: avoid deadlocks (especially from FileDeleteForm) + // T58776: avoid deadlocks (especially from FileDeleteForm) $logEntry->publish( $logid ); }, __METHOD__ @@ -2920,7 +2956,7 @@ class WikiPage implements Page, IDBAccessObject { $dbw->endAtomic( __METHOD__ ); - $this->doDeleteUpdates( $id, $content, $revision ); + $this->doDeleteUpdates( $id, $content, $revision, $user ); Hooks::run( 'ArticleDeleteComplete', [ &$wikiPageBeforeDelete, @@ -2934,8 +2970,8 @@ class WikiPage implements Page, IDBAccessObject { $status->value = $logid; // Show log excerpt on 404 pages rather than just a link - $cache = ObjectCache::getMainStashInstance(); - $key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) ); + $cache = MediaWikiServices::getInstance()->getMainObjectStash(); + $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) ); $cache->set( $key, 1, $cache::TTL_DAY ); return $status; @@ -2944,7 +2980,7 @@ class WikiPage implements Page, IDBAccessObject { /** * Lock the page row for this title+id and return page_latest (or 0) * - * @return integer Returns 0 if no row was found with this title+id + * @return int Returns 0 if no row was found with this title+id * @since 1.27 */ public function lockAndGetLatest() { @@ -2971,8 +3007,11 @@ class WikiPage implements Page, IDBAccessObject { * the required updates. This may be needed because $this->getContent() * may already return null when the page proper was deleted. * @param Revision|null $revision The latest page revision + * @param User|null $user The user that caused the deletion */ - public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) { + public function doDeleteUpdates( + $id, Content $content = null, Revision $revision = null, User $user = null + ) { try { $countable = $this->isCountable(); } catch ( Exception $ex ) { @@ -2982,7 +3021,9 @@ class WikiPage implements Page, IDBAccessObject { } // Update site status - DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) ); + DeferredUpdates::addUpdate( SiteStatsUpdate::factory( + [ 'edits' => 1, 'articles' => -$countable, 'pages' => -1 ] + ) ); // Delete pagelinks, update secondary indexes, etc $updates = $this->getDeletionUpdates( $content ); @@ -2990,16 +3031,18 @@ class WikiPage implements Page, IDBAccessObject { DeferredUpdates::addUpdate( $update ); } + $causeAgent = $user ? $user->getName() : 'unknown'; // Reparse any pages transcluding this page - LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' ); - + LinksUpdate::queueRecursiveJobsForTable( + $this->mTitle, 'templatelinks', 'delete-page', $causeAgent ); // Reparse any pages including this image if ( $this->mTitle->getNamespace() == NS_FILE ) { - LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' ); + LinksUpdate::queueRecursiveJobsForTable( + $this->mTitle, 'imagelinks', 'delete-page', $causeAgent ); } // Clear caches - WikiPage::onArticleDelete( $this->mTitle ); + self::onArticleDelete( $this->mTitle ); ResourceLoaderWikiModule::invalidateModuleCache( $this->mTitle, $revision, null, wfWikiID() ); @@ -3026,7 +3069,7 @@ class WikiPage implements Page, IDBAccessObject { * @param string $token Rollback token. * @param bool $bot If true, mark all reverted edits as bot. * - * @param array $resultDetails Array contains result-specific array of additional values + * @param array &$resultDetails Array contains result-specific array of additional values * 'alreadyrolled' : 'current' (rev) * success : 'summary' (str), 'current' (rev), 'target' (rev) * @@ -3078,7 +3121,7 @@ class WikiPage implements Page, IDBAccessObject { * @param string $summary Custom summary. Set to default summary if empty. * @param bool $bot If true, mark all reverted edits as bot. * - * @param array $resultDetails Contains result-specific array of additional values + * @param array &$resultDetails Contains result-specific array of additional values * @param User $guser The user performing the rollback * @param array|null $tags Change tags to apply to the rollback * Callers are responsible for permission checks @@ -3163,9 +3206,6 @@ class WikiPage implements Page, IDBAccessObject { // Trim spaces on user supplied text $summary = trim( $summary ); - // Truncate for whole multibyte characters. - $summary = $wgContLang->truncate( $summary, 255 ); - // Save $flags = EDIT_UPDATE | EDIT_INTERNAL; @@ -3180,6 +3220,10 @@ class WikiPage implements Page, IDBAccessObject { $targetContent = $target->getContent(); $changingContentModel = $targetContent->getModel() !== $current->getContentModel(); + if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) { + $tags[] = 'mw-rollback'; + } + // Actually store the edit $status = $this->doEditContent( $targetContent, @@ -3192,7 +3236,7 @@ class WikiPage implements Page, IDBAccessObject { ); // Set patrolling and bot flag on the edits, which gets rollbacked. - // This is done even on edit failure to have patrolling in that case (bug 62157). + // This is done even on edit failure to have patrolling in that case (T64157). $set = []; if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { // Mark all reverted edits as bot @@ -3256,7 +3300,8 @@ class WikiPage implements Page, IDBAccessObject { 'summary' => $summary, 'current' => $current, 'target' => $target, - 'newid' => $revId + 'newid' => $revId, + 'tags' => $tags ]; return []; @@ -3285,6 +3330,11 @@ class WikiPage implements Page, IDBAccessObject { MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title ); + // Invalidate caches of articles which include this page + DeferredUpdates::addUpdate( + new HTMLCacheUpdate( $title, 'templatelinks', 'page-create' ) + ); + if ( $title->getNamespace() == NS_CATEGORY ) { // Load the Category object, which will schedule a job to create // the category table row if necessary. Checking a replica DB is ok @@ -3301,6 +3351,8 @@ class WikiPage implements Page, IDBAccessObject { */ public static function onArticleDelete( Title $title ) { // Update existence markers on article/talk tabs... + // Clear Backlink cache first so that purge jobs use more up-to-date backlink information + BacklinkCache::get( $title )->clear(); $other = $title->getOtherPage(); $other->purgeSquid(); @@ -3321,7 +3373,9 @@ class WikiPage implements Page, IDBAccessObject { // Images if ( $title->getNamespace() == NS_FILE ) { - DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) ); + DeferredUpdates::addUpdate( + new HTMLCacheUpdate( $title, 'imagelinks', 'page-delete' ) + ); } // User talk pages @@ -3344,10 +3398,14 @@ class WikiPage implements Page, IDBAccessObject { */ public static function onArticleEdit( Title $title, Revision $revision = null ) { // Invalidate caches of articles which include this page - DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) ); + DeferredUpdates::addUpdate( + new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' ) + ); // Invalidate the caches of all pages which redirect here - DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) ); + DeferredUpdates::addUpdate( + new HTMLCacheUpdate( $title, 'redirect', 'page-edit' ) + ); MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title ); @@ -3357,7 +3415,7 @@ class WikiPage implements Page, IDBAccessObject { HTMLFileCache::clearFileCache( $title ); $revid = $revision ? $revision->getId() : null; - DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) { + DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) { InfoAction::invalidateCache( $title, $revid ); } ); } @@ -3436,7 +3494,7 @@ class WikiPage implements Page, IDBAccessObject { * * @param array $added The names of categories that were added * @param array $deleted The names of categories that were deleted - * @param integer $id Page ID (this should be the original deleted page ID) + * @param int $id Page ID (this should be the original deleted page ID) */ public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) { $id = $id ?: $this->getId(); @@ -3526,7 +3584,10 @@ class WikiPage implements Page, IDBAccessObject { ); foreach ( $rows as $row ) { $cat = Category::newFromRow( $row ); - $cat->refreshCounts(); + // T166757: do the update after this DB commit + DeferredUpdates::addCallableUpdate( function () use ( $cat ) { + $cat->refreshCounts(); + } ); } } } @@ -3652,7 +3713,7 @@ class WikiPage implements Page, IDBAccessObject { return $this->getTitle()->getCanonicalURL(); } - /* + /** * @param WANObjectCache $cache * @return string[] * @since 1.28