Merge "Add a change tag for edits that change the content model of a page"
[lhc/web/wiklou.git] / includes / page / WikiPage.php
index e1d9f99..4ad2e65 100644 (file)
@@ -21,6 +21,7 @@
  */
 
 use \MediaWiki\Logger\LoggerFactory;
+use \MediaWiki\MediaWikiServices;
 
 /**
  * Class representing a MediaWiki article and history.
@@ -82,6 +83,11 @@ 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.
@@ -134,7 +140,7 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @param int $id Article ID to load
         * @param string|int $from One of the following values:
-        *        - "fromdb" or WikiPage::READ_NORMAL to select from a slave database
+        *        - "fromdb" or WikiPage::READ_NORMAL to select from a replica DB
         *        - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database
         *
         * @return WikiPage|null
@@ -146,7 +152,7 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                $from = self::convertSelectType( $from );
-               $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
+               $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_REPLICA );
                $row = $db->selectRow(
                        'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
                if ( !$row ) {
@@ -161,7 +167,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @since 1.20
         * @param object $row Database row containing at least fields returned by selectFields().
         * @param string|int $from Source of $data:
-        *        - "fromdb" or WikiPage::READ_NORMAL: from a slave DB
+        *        - "fromdb" or WikiPage::READ_NORMAL: from a replica DB
         *        - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
         *        - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
         * @return WikiPage
@@ -346,7 +352,7 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @param object|string|int $from One of the following:
         *   - A DB query result object.
-        *   - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB.
+        *   - "fromdb" or WikiPage::READ_NORMAL to get from a replica DB.
         *   - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
         *   - "forupdate"  or WikiPage::READ_LOCKING to get from the master DB
         *     using SELECT FOR UPDATE.
@@ -365,7 +371,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
 
                        if ( !$data
-                               && $index == DB_SLAVE
+                               && $index == DB_REPLICA
                                && wfGetLB()->getServerCount() > 1
                                && wfGetLB()->hasOrMadeRecentMasterChanges()
                        ) {
@@ -374,7 +380,7 @@ class WikiPage implements Page, IDBAccessObject {
                                $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts );
                        }
                } else {
-                       // No idea from where the caller got this data, assume slave database.
+                       // No idea from where the caller got this data, assume replica DB.
                        $data = $from;
                        $from = self::READ_NORMAL;
                }
@@ -388,7 +394,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @since 1.20
         * @param object|bool $data DB row containing fields returned by selectFields() or false
         * @param string|int $from One of the following:
-        *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
+        *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a replica DB
         *        - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
         *        - "forupdate"  or WikiPage::READ_LOCKING if the data comes from
         *          the master DB using SELECT FOR UPDATE
@@ -459,7 +465,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @return bool
         */
        public function hasViewableContent() {
-               return $this->exists() || $this->mTitle->isAlwaysKnown();
+               return $this->mTitle->isKnown();
        }
 
        /**
@@ -552,9 +558,9 @@ class WikiPage implements Page, IDBAccessObject {
         */
        public function getOldestRevision() {
 
-               // Try using the slave database first, then try the master
+               // Try using the replica DB first, then try the master
                $continue = 2;
-               $db = wfGetDB( DB_SLAVE );
+               $db = wfGetDB( DB_REPLICA );
                $revSelectFields = Revision::selectFields();
 
                $row = null;
@@ -609,7 +615,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $flags = Revision::READ_LOCKING;
                } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
                        // Bug T93976: if page_latest was loaded from the master, fetch the
-                       // revision from there as well, as it may not exist yet on a slave DB.
+                       // revision from there as well, as it may not exist yet on a replica DB.
                        // Also, this keeps the queries in the same REPEATABLE-READ snapshot.
                        $flags = Revision::READ_LATEST;
                } else {
@@ -831,7 +837,7 @@ class WikiPage implements Page, IDBAccessObject {
                                // links.
                                $hasLinks = (bool)count( $editInfo->output->getLinks() );
                        } else {
-                               $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+                               $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
                                        [ 'pl_from' => $this->getId() ], __METHOD__ );
                        }
                }
@@ -856,7 +862,7 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                // Query the redirect table
-               $dbr = wfGetDB( DB_SLAVE );
+               $dbr = wfGetDB( DB_REPLICA );
                $row = $dbr->selectRow( 'redirect',
                        [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
                        [ 'rd_from' => $this->getId() ],
@@ -989,7 +995,7 @@ class WikiPage implements Page, IDBAccessObject {
        public function getContributors() {
                // @todo FIXME: This is expensive; cache this info somewhere.
 
-               $dbr = wfGetDB( DB_SLAVE );
+               $dbr = wfGetDB( DB_REPLICA );
 
                if ( $dbr->implicitGroupby() ) {
                        $realNameField = 'user_real_name';
@@ -1110,19 +1116,38 @@ class WikiPage implements Page, IDBAccessObject {
 
        /**
         * Perform the actions of a page purging
+        * @param integer $flags Bitfield of WikiPage::PURGE_* constants
         * @return bool
         */
-       public function doPurge() {
+       public function doPurge( $flags = self::PURGE_ALL ) {
                if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
                        return false;
                }
 
-               $this->mTitle->invalidateCache();
-               // Send purge after above page_touched update was committed
-               DeferredUpdates::addUpdate(
-                       new CdnCacheUpdate( $this->mTitle->getCdnUrls() ),
-                       DeferredUpdates::PRESEND
-               );
+               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
+                       );
+               }
+
+               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
+                       );
+               }
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        // @todo move this logic to MessageCache
@@ -1146,6 +1171,18 @@ 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
@@ -1386,7 +1423,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
-                       $dbr = wfGetDB( DB_SLAVE );
+                       $dbr = wfGetDB( 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
@@ -1595,10 +1632,15 @@ class WikiPage implements Page, IDBAccessObject {
         */
        public function doEditContent(
                Content $content, $summary, $flags = 0, $baseRevId = false,
-               User $user = null, $serialFormat = null, $tags = null
+               User $user = null, $serialFormat = null, $tags = []
        ) {
                global $wgUser, $wgUseAutomaticEditSummaries;
 
+               // Old default parameter for $tags was null
+               if ( $tags === null ) {
+                       $tags = [];
+               }
+
                // Low-level sanity check
                if ( $this->mTitle->getText() === '' ) {
                        throw new MWException( 'Something is trying to edit an article with an empty title' );
@@ -1638,6 +1680,10 @@ 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';
+               }
+
                // Provide autosummaries if one is not provided and autosummaries are enabled
                if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
                        $handler = $content->getContentHandler();
@@ -2110,7 +2156,7 @@ class WikiPage implements Page, IDBAccessObject {
                                // We get here if vary-revision is set. This means that this page references
                                // itself (such as via self-transclusion). In this case, we need to make sure
                                // that any such self-references refer to the newly-saved revision, and not
-                               // to the previous one, which could otherwise happen due to slave lag.
+                               // to the previous one, which could otherwise happen due to replica DB lag.
                                $oldCallback = $edit->popts->getCurrentRevisionCallback();
                                $edit->popts->setCurrentRevisionCallback(
                                        function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
@@ -2713,14 +2759,14 @@ class WikiPage implements Page, IDBAccessObject {
                $protectDescription = '';
 
                foreach ( array_filter( $limit ) as $action => $restrictions ) {
-                       # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
+                       # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
                        # All possible message keys are listed here for easier grepping:
                        # * restriction-create
                        # * restriction-edit
                        # * restriction-move
                        # * restriction-upload
                        $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
-                       # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
+                       # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
                        # with '' filtered out. All possible message keys are listed below:
                        # * protect-level-autoconfirmed
                        # * protect-level-sysop
@@ -2900,8 +2946,9 @@ class WikiPage implements Page, IDBAccessObject {
                        $bitfield |= Revision::DELETED_COMMENT;
                        $bitfield |= Revision::DELETED_USER;
                        $bitfield |= Revision::DELETED_RESTRICTED;
+                       $deletionFields = [ $dbw->addQuotes( $bitfield ) . ' AS deleted' ];
                } else {
-                       $bitfield = 'rev_deleted';
+                       $deletionFields = [ 'rev_deleted AS deleted' ];
                }
 
                // For now, shunt the revision data into the archive table.
@@ -2912,9 +2959,10 @@ class WikiPage implements Page, IDBAccessObject {
                // the rev_deleted field, which is reserved for this purpose.
 
                // Get all of the page revisions
+               $fields = array_diff( Revision::selectFields(), [ 'rev_deleted' ] );
                $res = $dbw->select(
                        'revision',
-                       Revision::selectFields(),
+                       array_merge( $fields, $deletionFields ),
                        [ 'rev_page' => $id ],
                        __METHOD__,
                        'FOR UPDATE'
@@ -2937,7 +2985,7 @@ class WikiPage implements Page, IDBAccessObject {
                                'ar_flags'      => '',
                                'ar_len'        => $row->rev_len,
                                'ar_page_id'    => $id,
-                               'ar_deleted'    => $bitfield,
+                               'ar_deleted'    => $row->deleted,
                                'ar_sha1'       => $row->rev_sha1,
                        ];
                        if ( $wgContentHandlerUseDB ) {
@@ -3232,9 +3280,12 @@ class WikiPage implements Page, IDBAccessObject {
                        $flags |= EDIT_FORCE_BOT;
                }
 
+               $targetContent = $target->getContent();
+               $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
+
                // Actually store the edit
                $status = $this->doEditContent(
-                       $target->getContent(),
+                       $targetContent,
                        $summary,
                        $flags,
                        $target->getId(),
@@ -3284,6 +3335,22 @@ class WikiPage implements Page, IDBAccessObject {
                        ] ];
                }
 
+               if ( $changingContentModel ) {
+                       // If the content model changed during the rollback,
+                       // make sure it gets logged to Special:Log/contentmodel
+                       $log = new ManualLogEntry( 'contentmodel', 'change' );
+                       $log->setPerformer( $guser );
+                       $log->setTarget( $this->mTitle );
+                       $log->setComment( $summary );
+                       $log->setParameters( [
+                               '4::oldmodel' => $current->getContentModel(),
+                               '5::newmodel' => $targetContent->getModel(),
+                       ] );
+
+                       $logId = $log->insert( $dbw );
+                       $log->publish( $logId );
+               }
+
                $revId = $statusRev->getId();
 
                Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
@@ -3319,9 +3386,11 @@ class WikiPage implements Page, IDBAccessObject {
                $title->purgeSquid();
                $title->deleteTitleProtection();
 
+               MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
                if ( $title->getNamespace() == NS_CATEGORY ) {
                        // Load the Category object, which will schedule a job to create
-                       // the category table row if necessary. Checking a slave is ok
+                       // the category table row if necessary. Checking a replica DB is ok
                        // here, in the worst case it'll run an unnecessary recount job on
                        // a category that probably doesn't have many members.
                        Category::newFromTitle( $title )->getID();
@@ -3344,6 +3413,8 @@ class WikiPage implements Page, IDBAccessObject {
                $title->touchLinks();
                $title->purgeSquid();
 
+               MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
                // File cache
                HTMLFileCache::clearFileCache( $title );
                InfoAction::invalidateCache( $title );
@@ -3387,6 +3458,8 @@ class WikiPage implements Page, IDBAccessObject {
                // Invalidate the caches of all pages which redirect here
                DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
 
+               MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
                // Purge CDN for this page only
                $title->purgeSquid();
                // Clear file cache for this page only
@@ -3412,7 +3485,7 @@ class WikiPage implements Page, IDBAccessObject {
                        return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
                }
 
-               $dbr = wfGetDB( DB_SLAVE );
+               $dbr = wfGetDB( DB_REPLICA );
                $res = $dbr->select( 'categorylinks',
                        [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
                        // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
@@ -3437,7 +3510,7 @@ class WikiPage implements Page, IDBAccessObject {
                        return [];
                }
 
-               $dbr = wfGetDB( DB_SLAVE );
+               $dbr = wfGetDB( DB_REPLICA );
                $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
                        [ 'cl_to' ],
                        [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
@@ -3675,4 +3748,15 @@ class WikiPage implements Page, IDBAccessObject {
                Hooks::run( 'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
                return $updates;
        }
+
+       /**
+        * Whether this content displayed on this page
+        * comes from the local database
+        *
+        * @since 1.28
+        * @return bool
+        */
+       public function isLocal() {
+               return true;
+       }
 }