Merge "Add GENDER support to protection null revision edit summary"
[lhc/web/wiklou.git] / includes / page / WikiPage.php
index 23e0887..dd78d19 100644 (file)
  * @file
  */
 
-/**
- * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
- */
-interface Page {
-}
+use \MediaWiki\Logger\LoggerFactory;
+use \MediaWiki\MediaWikiServices;
 
 /**
  * Class representing a MediaWiki article and history.
@@ -86,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.
@@ -94,13 +96,21 @@ class WikiPage implements Page, IDBAccessObject {
                $this->mTitle = $title;
        }
 
+       /**
+        * Makes sure that the mTitle object is cloned
+        * to the newly cloned WikiPage.
+        */
+       public function __clone() {
+               $this->mTitle = clone $this->mTitle;
+       }
+
        /**
         * Create a WikiPage object of the appropriate class for the given title.
         *
         * @param Title $title
         *
         * @throws MWException
-        * @return WikiPage Object of the appropriate type
+        * @return WikiPage|WikiCategoryPage|WikiFilePage
         */
        public static function factory( Title $title ) {
                $ns = $title->getNamespace();
@@ -111,6 +121,11 @@ class WikiPage implements Page, IDBAccessObject {
                        throw new MWException( "Invalid or virtual namespace $ns given." );
                }
 
+               $page = null;
+               if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) {
+                       return $page;
+               }
+
                switch ( $ns ) {
                        case NS_FILE:
                                $page = new WikiFilePage( $title );
@@ -130,7 +145,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
@@ -142,9 +157,9 @@ 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(), array( 'page_id' => $id ), __METHOD__ );
+                       'page', self::selectFields(), [ 'page_id' => $id ], __METHOD__ );
                if ( !$row ) {
                        return null;
                }
@@ -157,7 +172,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
@@ -189,18 +204,12 @@ class WikiPage implements Page, IDBAccessObject {
        }
 
        /**
-        * Returns overrides for action handlers.
-        * Classes listed here will be used instead of the default one when
-        * (and only when) $wgActions[$action] === true. This allows subclasses
-        * to override the default behavior.
-        *
         * @todo Move this UI stuff somewhere else
         *
-        * @return array
+        * @see ContentHandler::getActionOverrides
         */
        public function getActionOverrides() {
-               $content_handler = $this->getContentHandler();
-               return $content_handler->getActionOverrides();
+               return $this->getContentHandler()->getActionOverrides();
        }
 
        /**
@@ -272,7 +281,7 @@ class WikiPage implements Page, IDBAccessObject {
        public static function selectFields() {
                global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
 
-               $fields = array(
+               $fields = [
                        'page_id',
                        'page_namespace',
                        'page_title',
@@ -284,7 +293,7 @@ class WikiPage implements Page, IDBAccessObject {
                        'page_links_updated',
                        'page_latest',
                        'page_len',
-               );
+               ];
 
                if ( $wgContentHandlerUseDB ) {
                        $fields[] = 'page_content_model';
@@ -304,14 +313,14 @@ class WikiPage implements Page, IDBAccessObject {
         * @param array $options
         * @return object|bool Database result resource, or false on failure
         */
-       protected function pageData( $dbr, $conditions, $options = array() ) {
+       protected function pageData( $dbr, $conditions, $options = [] ) {
                $fields = self::selectFields();
 
-               Hooks::run( 'ArticlePageDataBefore', array( &$this, &$fields ) );
+               Hooks::run( 'ArticlePageDataBefore', [ &$this, &$fields ] );
 
                $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
 
-               Hooks::run( 'ArticlePageDataAfter', array( &$this, &$row ) );
+               Hooks::run( 'ArticlePageDataAfter', [ &$this, &$row ] );
 
                return $row;
        }
@@ -325,10 +334,10 @@ class WikiPage implements Page, IDBAccessObject {
         * @param array $options
         * @return object|bool Database result resource, or false on failure
         */
-       public function pageDataFromTitle( $dbr, $title, $options = array() ) {
-               return $this->pageData( $dbr, array(
+       public function pageDataFromTitle( $dbr, $title, $options = [] ) {
+               return $this->pageData( $dbr, [
                        'page_namespace' => $title->getNamespace(),
-                       'page_title' => $title->getDBkey() ), $options );
+                       'page_title' => $title->getDBkey() ], $options );
        }
 
        /**
@@ -339,8 +348,8 @@ class WikiPage implements Page, IDBAccessObject {
         * @param array $options
         * @return object|bool Database result resource, or false on failure
         */
-       public function pageDataFromId( $dbr, $id, $options = array() ) {
-               return $this->pageData( $dbr, array( 'page_id' => $id ), $options );
+       public function pageDataFromId( $dbr, $id, $options = [] ) {
+               return $this->pageData( $dbr, [ 'page_id' => $id ], $options );
        }
 
        /**
@@ -348,7 +357,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.
@@ -367,7 +376,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()
                        ) {
@@ -376,7 +385,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;
                }
@@ -390,7 +399,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
@@ -461,7 +470,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @return bool
         */
        public function hasViewableContent() {
-               return $this->exists() || $this->mTitle->isAlwaysKnown();
+               return $this->mTitle->isKnown();
        }
 
        /**
@@ -489,15 +498,23 @@ class WikiPage implements Page, IDBAccessObject {
         */
        public function getContentModel() {
                if ( $this->exists() ) {
-                       // look at the revision's actual content model
-                       $rev = $this->getRevision();
-
-                       if ( $rev !== null ) {
-                               return $rev->getContentModel();
-                       } else {
-                               $title = $this->mTitle->getPrefixedDBkey();
-                               wfWarn( "Page $title exists but has no (visible) revisions!" );
-                       }
+                       $cache = ObjectCache::getMainWANInstance();
+
+                       return $cache->getWithSetCallback(
+                               $cache->makeKey( 'page', 'content-model', $this->getLatest() ),
+                               $cache::TTL_MONTH,
+                               function () {
+                                       $rev = $this->getRevision();
+                                       if ( $rev ) {
+                                               // Look at the revision's actual content model
+                                               return $rev->getContentModel();
+                                       } else {
+                                               $title = $this->mTitle->getPrefixedDBkey();
+                                               wfWarn( "Page $title exists but has no (visible) revisions!" );
+                                               return $this->mTitle->getContentModel();
+                                       }
+                               }
+                       );
                }
 
                // use the default model for this page
@@ -506,13 +523,13 @@ class WikiPage implements Page, IDBAccessObject {
 
        /**
         * Loads page_touched and returns a value indicating if it should be used
-        * @return bool True if not a redirect
+        * @return bool True if this page exists and is not a redirect
         */
        public function checkTouched() {
                if ( !$this->mDataLoaded ) {
                        $this->loadPageData();
                }
-               return !$this->mIsRedirect;
+               return ( $this->mId && !$this->mIsRedirect );
        }
 
        /**
@@ -554,25 +571,25 @@ 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;
                while ( $continue ) {
                        $row = $db->selectRow(
-                               array( 'page', 'revision' ),
+                               [ 'page', 'revision' ],
                                $revSelectFields,
-                               array(
+                               [
                                        'page_namespace' => $this->mTitle->getNamespace(),
                                        'page_title' => $this->mTitle->getDBkey(),
                                        'rev_page = page_id'
-                               ),
+                               ],
                                __METHOD__,
-                               array(
+                               [
                                        'ORDER BY' => 'rev_timestamp ASC'
-                               )
+                               ]
                        );
 
                        if ( $row ) {
@@ -609,15 +626,18 @@ class WikiPage implements Page, IDBAccessObject {
                        // happened after the first S1 SELECT.
                        // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
                        $flags = Revision::READ_LOCKING;
+                       $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
                } 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;
+                       $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
                } else {
-                       $flags = 0;
+                       $dbr = wfGetDB( DB_REPLICA );
+                       $revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
                }
-               $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
+
                if ( $revision ) { // sanity
                        $this->setLastEdit( $revision );
                }
@@ -678,7 +698,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @deprecated since 1.21, getContent() should be used instead.
         */
        public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
@@ -687,18 +707,6 @@ class WikiPage implements Page, IDBAccessObject {
                return false;
        }
 
-       /**
-        * Get the text of the current revision. No side-effects...
-        *
-        * @return string|bool The text of the current revision. False on failure
-        * @deprecated since 1.21, getContent() should be used instead.
-        */
-       public function getRawText() {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
-
-               return $this->getText( Revision::RAW );
-       }
-
        /**
         * @return string MW timestamp of last article revision
         */
@@ -845,8 +853,8 @@ class WikiPage implements Page, IDBAccessObject {
                                // links.
                                $hasLinks = (bool)count( $editInfo->output->getLinks() );
                        } else {
-                               $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
-                                       array( 'pl_from' => $this->getId() ), __METHOD__ );
+                               $hasLinks = (bool)wfGetDB( DB_REPLICA )->selectField( 'pagelinks', 1,
+                                       [ 'pl_from' => $this->getId() ], __METHOD__ );
                        }
                }
 
@@ -870,10 +878,10 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                // Query the redirect table
-               $dbr = wfGetDB( DB_SLAVE );
+               $dbr = wfGetDB( DB_REPLICA );
                $row = $dbr->selectRow( 'redirect',
-                       array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ),
-                       array( 'rd_from' => $this->getId() ),
+                       [ 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
+                       [ 'rd_from' => $this->getId() ],
                        __METHOD__
                );
 
@@ -909,9 +917,13 @@ 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 );
-               } );
+               DeferredUpdates::addCallableUpdate(
+                       function () use ( $that, $retval, $latest ) {
+                               $that->insertRedirectEntry( $retval, $latest );
+                       },
+                       DeferredUpdates::POSTSEND,
+                       wfGetDB( DB_MASTER )
+               );
 
                return $retval;
        }
@@ -927,14 +939,14 @@ class WikiPage implements Page, IDBAccessObject {
 
                if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
                        $dbw->replace( 'redirect',
-                               array( 'rd_from' ),
-                               array(
+                               [ 'rd_from' ],
+                               [
                                        'rd_from' => $this->getId(),
                                        'rd_namespace' => $rt->getNamespace(),
                                        'rd_title' => $rt->getDBkey(),
                                        'rd_fragment' => $rt->getFragment(),
                                        'rd_interwiki' => $rt->getInterwiki(),
-                               ),
+                               ],
                                __METHOD__
                        );
                }
@@ -969,7 +981,7 @@ class WikiPage implements Page, IDBAccessObject {
                                // This can be hard to reverse and may produce loops,
                                // so they may be disabled in the site configuration.
                                $source = $this->mTitle->getFullURL( 'redirect=no' );
-                               return $rt->getFullURL( array( 'rdfrom' => $source ) );
+                               return $rt->getFullURL( [ 'rdfrom' => $source ] );
                        } else {
                                // External pages without "local" bit set are not valid
                                // redirect targets
@@ -999,7 +1011,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';
@@ -1007,16 +1019,16 @@ class WikiPage implements Page, IDBAccessObject {
                        $realNameField = 'MIN(user_real_name) AS user_real_name';
                }
 
-               $tables = array( 'revision', 'user' );
+               $tables = [ 'revision', 'user' ];
 
-               $fields = array(
+               $fields = [
                        'user_id' => 'rev_user',
                        'user_name' => 'rev_user_text',
                        $realNameField,
                        'timestamp' => 'MAX(rev_timestamp)',
-               );
+               ];
 
-               $conds = array( 'rev_page' => $this->getId() );
+               $conds = [ 'rev_page' => $this->getId() ];
 
                // The user who made the top revision gets credited as "this page was last edited by
                // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
@@ -1030,14 +1042,14 @@ class WikiPage implements Page, IDBAccessObject {
                // Username hidden?
                $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
 
-               $jconds = array(
-                       'user' => array( 'LEFT JOIN', 'rev_user = user_id' ),
-               );
+               $jconds = [
+                       'user' => [ 'LEFT JOIN', 'rev_user = user_id' ],
+               ];
 
-               $options = array(
-                       'GROUP BY' => array( 'rev_user', 'rev_user_text' ),
+               $options = [
+                       'GROUP BY' => [ 'rev_user', 'rev_user_text' ],
                        'ORDER BY' => 'timestamp DESC',
-               );
+               ];
 
                $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
                return new UserArrayFromResult( $res );
@@ -1065,14 +1077,16 @@ 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)
-        *
-        * @return ParserOutput|bool ParserOutput or false if the revision was not found
+        * @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( ParserOptions $parserOptions, $oldid = null ) {
-
-               $useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
+       public function getParserOutput(
+               ParserOptions $parserOptions, $oldid = null, $forceParse = false
+       ) {
+               $useParserCache =
+                       ( !$forceParse ) && $this->shouldCheckParserCache( $parserOptions, $oldid );
                wfDebug( __METHOD__ .
                        ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
                if ( $parserOptions->getStubThreshold() ) {
@@ -1106,7 +1120,7 @@ class WikiPage implements Page, IDBAccessObject {
                        return;
                }
 
-               Hooks::run( 'PageViewUpdates', array( $this, $user ) );
+               Hooks::run( 'PageViewUpdates', [ $this, $user ] );
                // Update newtalk / watchlist notification status
                try {
                        $user->clearNotification( $this->mTitle, $oldid );
@@ -1118,24 +1132,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() {
-               if ( !Hooks::run( 'ArticlePurge', array( &$this ) ) ) {
+       public function doPurge( $flags = self::PURGE_ALL ) {
+               if ( !Hooks::run( 'ArticlePurge', [ &$this ] ) ) {
                        return false;
                }
 
-               $title = $this->mTitle;
-               wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) {
-                       // Invalidate the cache in auto-commit mode
-                       $title->invalidateCache();
-               } );
+               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
+                       );
+               }
 
-               // Send purge after above page_touched update was committed
-               DeferredUpdates::addUpdate(
-                       new SquidUpdate( $title->getSquidURLs() ),
-                       DeferredUpdates::PRESEND
-               );
+               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
@@ -1158,6 +1186,19 @@ 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
@@ -1166,13 +1207,18 @@ class WikiPage implements Page, IDBAccessObject {
         * Best if all done inside a transaction.
         *
         * @param IDatabase $dbw
-        * @return int|bool The newly created page_id key; false if the title already existed
+        * @param int|null $pageId Custom page ID that will be used for the insert statement
+        *
+        * @return bool|int The newly created page_id key; false if the row was not
+        *   inserted, e.g. because the title already existed or because the specified
+        *   page ID is already in use.
         */
-       public function insertOn( $dbw ) {
+       public function insertOn( $dbw, $pageId = null ) {
+               $pageIdForInsert = $pageId ?: $dbw->nextSequenceValue( 'page_page_id_seq' );
                $dbw->insert(
                        'page',
-                       array(
-                               'page_id'           => $dbw->nextSequenceValue( 'page_page_id_seq' ),
+                       [
+                               'page_id'           => $pageIdForInsert,
                                'page_namespace'    => $this->mTitle->getNamespace(),
                                'page_title'        => $this->mTitle->getDBkey(),
                                'page_restrictions' => '',
@@ -1182,13 +1228,13 @@ class WikiPage implements Page, IDBAccessObject {
                                'page_touched'      => $dbw->timestamp(),
                                'page_latest'       => 0, // Fill this in shortly...
                                'page_len'          => 0, // Fill this in shortly...
-                       ),
+                       ],
                        __METHOD__,
                        'IGNORE'
                );
 
                if ( $dbw->affectedRows() > 0 ) {
-                       $newid = $dbw->insertId();
+                       $newid = $pageId ?: $dbw->insertId();
                        $this->mId = $newid;
                        $this->mTitle->resetArticleID( $newid );
 
@@ -1227,20 +1273,20 @@ class WikiPage implements Page, IDBAccessObject {
                $len = $content ? $content->getSize() : 0;
                $rt = $content ? $content->getUltimateRedirectTarget() : null;
 
-               $conditions = array( 'page_id' => $this->getId() );
+               $conditions = [ 'page_id' => $this->getId() ];
 
                if ( !is_null( $lastRevision ) ) {
                        // An extra check against threads stepping on each other
                        $conditions['page_latest'] = $lastRevision;
                }
 
-               $row = array( /* SET */
+               $row = [ /* SET */
                        'page_latest'      => $revision->getId(),
                        'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
                        'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
                        'page_is_redirect' => $rt !== null ? 1 : 0,
                        'page_len'         => $len,
-               );
+               ];
 
                if ( $wgContentHandlerUseDB ) {
                        $row['page_content_model'] = $revision->getContentModel();
@@ -1296,7 +1342,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $this->insertRedirectEntry( $redirectTitle );
                } else {
                        // This is not a redirect, remove row from redirect table
-                       $where = array( 'rd_from' => $this->getId() );
+                       $where = [ 'rd_from' => $this->getId() ];
                        $dbw->delete( 'redirect', $where, __METHOD__ );
                }
 
@@ -1320,11 +1366,11 @@ class WikiPage implements Page, IDBAccessObject {
        public function updateIfNewerOn( $dbw, $revision ) {
 
                $row = $dbw->selectRow(
-                       array( 'revision', 'page' ),
-                       array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ),
-                       array(
+                       [ 'revision', 'page' ],
+                       [ 'rev_id', 'rev_timestamp', 'page_is_redirect' ],
+                       [
                                'page_id' => $this->getId(),
-                               'page_latest=rev_id' ),
+                               'page_latest=rev_id' ],
                        __METHOD__ );
 
                if ( $row ) {
@@ -1359,76 +1405,6 @@ class WikiPage implements Page, IDBAccessObject {
                return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
        }
 
-       /**
-        * Get the text that needs to be saved in order to undo all revisions
-        * between $undo and $undoafter. Revisions must belong to the same page,
-        * must exist and must not be deleted
-        * @param Revision $undo
-        * @param Revision $undoafter Must be an earlier revision than $undo
-        * @return string|bool String on success, false on failure
-        * @deprecated since 1.21: use ContentHandler::getUndoContent() instead.
-        */
-       public function getUndoText( Revision $undo, Revision $undoafter = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
-
-               $this->loadLastEdit();
-
-               if ( $this->mLastRevision ) {
-                       if ( is_null( $undoafter ) ) {
-                               $undoafter = $undo->getPrevious();
-                       }
-
-                       $handler = $this->getContentHandler();
-                       $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
-
-                       if ( !$undone ) {
-                               return false;
-                       } else {
-                               return ContentHandler::getContentText( $undone );
-                       }
-               }
-
-               return false;
-       }
-
-       /**
-        * @param string|number|null|bool $sectionId Section identifier as a number or string
-        * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
-        * or 'new' for a new section.
-        * @param string $text New text of the section.
-        * @param string $sectionTitle New section's subject, only if $section is "new".
-        * @param string $edittime Revision timestamp or null to use the current revision.
-        *
-        * @throws MWException
-        * @return string|null New complete article text, or null if error.
-        *
-        * @deprecated since 1.21, use replaceSectionAtRev() instead
-        */
-       public function replaceSection( $sectionId, $text, $sectionTitle = '',
-               $edittime = null
-       ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
-
-               // NOTE: keep condition in sync with condition in replaceSectionContent!
-               if ( strval( $sectionId ) === '' ) {
-                       // Whole-page edit; let the whole text through
-                       return $text;
-               }
-
-               if ( !$this->supportsSections() ) {
-                       throw new MWException( "sections not supported for content model " .
-                               $this->getContentHandler()->getModelID() );
-               }
-
-               // could even make section title, but that's not required.
-               $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() );
-
-               $newContent = $this->replaceSectionContent( $sectionId, $sectionContent, $sectionTitle,
-                       $edittime );
-
-               return ContentHandler::getContentText( $newContent );
-       }
-
        /**
         * Returns true if this page's content model supports sections.
         *
@@ -1463,7 +1439,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
@@ -1570,6 +1546,8 @@ class WikiPage implements Page, IDBAccessObject {
         *          Mark the edit a "bot" edit regardless of user rights
         *      EDIT_AUTOSUMMARY
         *          Fill in blank summaries with generated text where possible
+        *      EDIT_INTERNAL
+        *          Signal that the page retrieve/save cycle happened entirely in this request.
         *
         * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
         * article will be detected. If EDIT_UPDATE is specified and the article
@@ -1604,7 +1582,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @deprecated since 1.21: use doEditContent() instead.
         */
        public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                $content = ContentHandler::makeContent( $text, $this->getTitle() );
 
@@ -1630,6 +1608,8 @@ class WikiPage implements Page, IDBAccessObject {
         *          Mark the edit a "bot" edit regardless of user rights
         *      EDIT_AUTOSUMMARY
         *          Fill in blank summaries with generated text where possible
+        *      EDIT_INTERNAL
+        *          Signal that the page retrieve/save cycle happened entirely in this request.
         *
         * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
         * article will be detected. If EDIT_UPDATE is specified and the article
@@ -1644,6 +1624,9 @@ class WikiPage implements Page, IDBAccessObject {
         * @param User $user The user doing the edit
         * @param string $serialFormat Format for storing the content in the
         *   database.
+        * @param array|null $tags Change tags to apply to this edit
+        * Callers are responsible for permission checks
+        * (with ChangeTags::canAddTagsAccompanyingChange)
         *
         * @throws MWException
         * @return Status Possible errors:
@@ -1665,10 +1648,15 @@ class WikiPage implements Page, IDBAccessObject {
         */
        public function doEditContent(
                Content $content, $summary, $flags = 0, $baseRevId = false,
-               User $user = null, $serialFormat = 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' );
@@ -1690,12 +1678,12 @@ class WikiPage implements Page, IDBAccessObject {
                $flags = $this->checkFlags( $flags );
 
                // Trigger pre-save hook (using provided edit summary)
-               $hookStatus = Status::newGood( array() );
-               $hook_args = array( &$this, &$user, &$content, &$summary,
-                                                       $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus );
+               $hookStatus = Status::newGood( [] );
+               $hook_args = [ &$this, &$user, &$content, &$summary,
+                                                       $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
                // Check if the hook rejected the attempted save
                if ( !Hooks::run( 'PageContentSave', $hook_args )
-                       || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args )
+                       || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args, '1.21' )
                ) {
                        if ( $hookStatus->isOK() ) {
                                // Hook returned false but didn't call fatal(); use generic message
@@ -1708,16 +1696,27 @@ 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();
                        $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
 
+               // Avoid statsd noise and wasted cycles check the edit stash (T136678)
+               if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
+                       $useCache = false;
+               } else {
+                       $useCache = true;
+               }
+
                // Get the pre-save transform content and final parser output
-               $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat );
+               $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
                $pstContent = $editInfo->pstContent; // Content object
-               $meta = array(
+               $meta = [
                        'bot' => ( $flags & EDIT_FORCE_BOT ),
                        'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
                        'serialized' => $editInfo->pst,
@@ -1727,8 +1726,9 @@ class WikiPage implements Page, IDBAccessObject {
                        'oldContent' => $old_content,
                        'oldId' => $this->getLatest(),
                        'oldIsRedirect' => $this->isRedirect(),
-                       'oldCountable' => $this->isCountable()
-               );
+                       'oldCountable' => $this->isCountable(),
+                       'tags' => ( $tags !== null ) ? (array)$tags : []
+               ];
 
                // Actually create the revision and create/update the page
                if ( $flags & EDIT_UPDATE ) {
@@ -1737,13 +1737,6 @@ class WikiPage implements Page, IDBAccessObject {
                        $status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
                }
 
-               // Trigger post-save hook
-               $revision = $status->value['revision']; // new revision
-               $hook_args = array( &$this, &$user, $pstContent, $summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
-               ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
-               Hooks::run( 'PageContentSaveComplete', $hook_args );
-
                // Promote user to any groups they meet the criteria for
                DeferredUpdates::addCallableUpdate( function () use ( $user ) {
                        $user->addAutopromoteOnceGroups( 'onEdit' );
@@ -1771,7 +1764,7 @@ class WikiPage implements Page, IDBAccessObject {
                global $wgUseRCPatrol;
 
                // Update article, but only if changed.
-               $status = Status::newGood( array( 'new' => false, 'revision' => null ) );
+               $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
 
                // Convenience variables
                $now = wfTimestampNow();
@@ -1791,7 +1784,7 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                // @TODO: pass content object?!
-               $revision = new Revision( array(
+               $revision = new Revision( [
                        'page'       => $this->getId(),
                        'title'      => $this->mTitle, // for determining the default content model
                        'comment'    => $summary,
@@ -1804,10 +1797,12 @@ class WikiPage implements Page, IDBAccessObject {
                        'timestamp'  => $now,
                        'content_model' => $content->getModel(),
                        'content_format' => $meta['serialFormat'],
-               ) );
+               ] );
 
                $changed = !$content->equals( $oldContent );
 
+               $dbw = wfGetDB( DB_MASTER );
+
                if ( $changed ) {
                        $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
                        $status->merge( $prepStatus );
@@ -1815,14 +1810,13 @@ class WikiPage implements Page, IDBAccessObject {
                                return $status;
                        }
 
-                       $dbw = wfGetDB( DB_MASTER );
-                       $dbw->begin( __METHOD__ );
+                       $dbw->startAtomic( __METHOD__ );
                        // Get the latest page_latest value while locking it.
                        // Do a CAS style check to see if it's the same as when this method
                        // started. If it changed then bail out before touching the DB.
                        $latestNow = $this->lockAndGetLatest();
                        if ( $latestNow != $oldid ) {
-                               $dbw->commit( __METHOD__ );
+                               $dbw->endAtomic( __METHOD__ );
                                // Page updated or deleted in the mean time
                                $status->fatal( 'edit-conflict' );
 
@@ -1838,12 +1832,11 @@ class WikiPage implements Page, IDBAccessObject {
                        $revisionId = $revision->insertOn( $dbw );
                        // Update page_latest and friends to reflect the new revision
                        if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
-                               $dbw->rollback( __METHOD__ );
                                throw new MWException( "Failed to update page row to use new revision." );
                        }
 
                        Hooks::run( 'NewRevisionFromEditComplete',
-                               array( $this, $revision, $meta['baseRevId'], $user ) );
+                               [ $this, $revision, $meta['baseRevId'], $user ] );
 
                        // Update recentchanges
                        if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
@@ -1864,31 +1857,25 @@ class WikiPage implements Page, IDBAccessObject {
                                        $oldContent ? $oldContent->getSize() : 0,
                                        $newsize,
                                        $revisionId,
-                                       $patrolled
+                                       $patrolled,
+                                       $meta['tags']
                                );
                        }
 
                        $user->incEditCount();
 
-                       $dbw->commit( __METHOD__ );
+                       $dbw->endAtomic( __METHOD__ );
                        $this->mTimestamp = $now;
                } else {
                        // Bug 32948: revision ID must be set to page {{REVISIONID}} and
-                       // related variables correctly
+                       // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
                        $revision->setId( $this->getLatest() );
+                       $revision->setUserIdAndName(
+                               $this->getUser( Revision::RAW ),
+                               $this->getUserText( Revision::RAW )
+                       );
                }
 
-               // Update links tables, site stats, etc.
-               $this->doEditUpdates(
-                       $revision,
-                       $user,
-                       array(
-                               'changed' => $changed,
-                               'oldcountable' => $meta['oldCountable'],
-                               'oldrevision' => $meta['oldRevision']
-                       )
-               );
-
                if ( $changed ) {
                        // Return the new revision to the caller
                        $status->value['revision'] = $revision;
@@ -1899,6 +1886,35 @@ class WikiPage implements Page, IDBAccessObject {
                        $this->mTitle->invalidateCache( $now );
                }
 
+               // Do secondary updates once the main changes have been committed...
+               DeferredUpdates::addUpdate(
+                       new AtomicSectionUpdate(
+                               $dbw,
+                               __METHOD__,
+                               function () use (
+                                       $revision, &$user, $content, $summary, &$flags,
+                                       $changed, $meta, &$status
+                               ) {
+                                       // Update links tables, site stats, etc.
+                                       $this->doEditUpdates(
+                                               $revision,
+                                               $user,
+                                               [
+                                                       'changed' => $changed,
+                                                       'oldcountable' => $meta['oldCountable'],
+                                                       'oldrevision' => $meta['oldRevision']
+                                               ]
+                                       );
+                                       // Trigger post-save hook
+                                       $params = [ &$this, &$user, $content, $summary, $flags & EDIT_MINOR,
+                                               null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
+                                       ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
+                                       Hooks::run( 'PageContentSaveComplete', $params );
+                               }
+                       ),
+                       DeferredUpdates::PRESEND
+               );
+
                return $status;
        }
 
@@ -1919,7 +1935,7 @@ class WikiPage implements Page, IDBAccessObject {
        ) {
                global $wgUseRCPatrol, $wgUseNPPatrol;
 
-               $status = Status::newGood( array( 'new' => true, 'revision' => null ) );
+               $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
 
                $now = wfTimestampNow();
                $newsize = $content->getSize();
@@ -1930,12 +1946,12 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                $dbw = wfGetDB( DB_MASTER );
-               $dbw->begin( __METHOD__ );
+               $dbw->startAtomic( __METHOD__ );
 
                // Add the page record unless one already exists for the title
                $newid = $this->insertOn( $dbw );
                if ( $newid === false ) {
-                       $dbw->commit( __METHOD__ ); // nothing inserted
+                       $dbw->endAtomic( __METHOD__ ); // nothing inserted
                        $status->fatal( 'edit-already-exists' );
 
                        return $status; // nothing done
@@ -1947,7 +1963,7 @@ class WikiPage implements Page, IDBAccessObject {
                // unless they actually try to catch exceptions (which is rare).
 
                // @TODO: pass content object?!
-               $revision = new Revision( array(
+               $revision = new Revision( [
                        'page'       => $newid,
                        'title'      => $this->mTitle, // for determining the default content model
                        'comment'    => $summary,
@@ -1959,17 +1975,16 @@ class WikiPage implements Page, IDBAccessObject {
                        'timestamp'  => $now,
                        'content_model' => $content->getModel(),
                        'content_format' => $meta['serialFormat'],
-               ) );
+               ] );
 
                // Save the revision text...
                $revisionId = $revision->insertOn( $dbw );
                // Update the page record with revision data
                if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
-                       $dbw->rollback( __METHOD__ );
                        throw new MWException( "Failed to update page row to use new revision." );
                }
 
-               Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
+               Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
 
                // Update recentchanges
                if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
@@ -1987,26 +2002,44 @@ class WikiPage implements Page, IDBAccessObject {
                                '',
                                $newsize,
                                $revisionId,
-                               $patrolled
+                               $patrolled,
+                               $meta['tags']
                        );
                }
 
                $user->incEditCount();
 
-               $dbw->commit( __METHOD__ );
+               $dbw->endAtomic( __METHOD__ );
                $this->mTimestamp = $now;
 
-               // Update links, etc.
-               $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
-
-               $hook_args = array( &$this, &$user, $content, $summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, $revision );
-               ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
-               Hooks::run( 'PageContentInsertComplete', $hook_args );
-
                // Return the new revision to the caller
                $status->value['revision'] = $revision;
 
+               // Do secondary updates once the main changes have been committed...
+               DeferredUpdates::addUpdate(
+                       new AtomicSectionUpdate(
+                               $dbw,
+                               __METHOD__,
+                               function () use (
+                                       $revision, &$user, $content, $summary, &$flags, $meta, &$status
+                               ) {
+                                       // Update links, etc.
+                                       $this->doEditUpdates( $revision, $user, [ 'created' => true ] );
+                                       // Trigger post-create hook
+                                       $params = [ &$this, &$user, $content, $summary,
+                                               $flags & EDIT_MINOR, null, null, &$flags, $revision ];
+                                       ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $params, '1.21' );
+                                       Hooks::run( 'PageContentInsertComplete', $params );
+                                       // Trigger post-save hook
+                                       $params = array_merge( $params, [ &$status, $meta['baseRevId'] ] );
+                                       ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params, '1.21' );
+                                       Hooks::run( 'PageContentSaveComplete', $params );
+
+                               }
+                       ),
+                       DeferredUpdates::PRESEND
+               );
+
                return $status;
        }
 
@@ -2047,7 +2080,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @return object
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
                $content = ContentHandler::makeContent( $text, $this->getTitle() );
                return $this->prepareContentForEdit( $content, $revid, $user );
        }
@@ -2095,7 +2128,7 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                if ( $this->mPreparedEdit
-                       && $this->mPreparedEdit->newContent
+                       && isset( $this->mPreparedEdit->newContent )
                        && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
                        && $this->mPreparedEdit->format == $serialFormat
@@ -2111,15 +2144,15 @@ class WikiPage implements Page, IDBAccessObject {
                        : false;
 
                $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
-               Hooks::run( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
+               Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
 
-               $edit = (object)array();
+               $edit = (object)[];
                if ( $cachedEdit ) {
                        $edit->timestamp = $cachedEdit->timestamp;
                } else {
                        $edit->timestamp = wfTimestampNow();
                }
-               // @note: $cachedEdit is not used if the rev ID was referenced in the text
+               // @note: $cachedEdit is safely not used if the rev ID was referenced in the text
                $edit->revid = $revid;
 
                if ( $cachedEdit ) {
@@ -2139,7 +2172,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 ) {
@@ -2150,6 +2183,16 @@ class WikiPage implements Page, IDBAccessObject {
                                                }
                                        }
                                );
+                       } else {
+                               // Try to avoid a second parse if {{REVISIONID}} is used
+                               $edit->popts->setSpeculativeRevIdCallback( function () {
+                                       return 1 + (int)wfGetDB( DB_MASTER )->selectField(
+                                               'revision',
+                                               'MAX(rev_id)',
+                                               [],
+                                               __METHOD__
+                                       );
+                               } );
                        }
                        $edit->output = $edit->pstContent
                                ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
@@ -2168,7 +2211,13 @@ class WikiPage implements Page, IDBAccessObject {
                        : '';
                $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
 
+               if ( $edit->output ) {
+                       $edit->output->setCacheTime( wfTimestampNow() );
+               }
+
+               // Process cache the result
                $this->mPreparedEdit = $edit;
+
                return $edit;
        }
 
@@ -2193,30 +2242,45 @@ class WikiPage implements Page, IDBAccessObject {
         *     is true, do update the article count
         *   - 'no-change': don't update the article count, ever
         */
-       public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
-               global $wgRCWatchCategoryMembership;
+       public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
+               global $wgRCWatchCategoryMembership, $wgContLang;
 
-               $options += array(
+               $options += [
                        'changed' => true,
                        'created' => false,
                        'moved' => false,
                        'restored' => false,
                        'oldrevision' => null,
                        'oldcountable' => null
-               );
+               ];
                $content = $revision->getContent();
 
-               // Parse the text
-               // Be careful not to do pre-save transform twice: $text is usually
-               // already pre-save transformed once.
-               if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
-                       wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
-                       $editInfo = $this->prepareContentForEdit( $content, $revision, $user );
+               $logger = LoggerFactory::getInstance( 'SaveParse' );
+
+               // See if the parser output before $revision was inserted is still valid
+               $editInfo = false;
+               if ( !$this->mPreparedEdit ) {
+                       $logger->debug( __METHOD__ . ": No prepared edit...\n" );
+               } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
+                       $logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
+               } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
+                       && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
+               ) {
+                       $logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
+               } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
+                       $logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
                } else {
-                       wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
+                       wfDebug( __METHOD__ . ": Using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
                }
 
+               if ( !$editInfo ) {
+                       // Parse the text again if needed. Be careful not to do pre-save transform twice:
+                       // $text is usually already pre-save transformed once. Avoid using the edit stash
+                       // as any prepared content from there or in doEditContent() was already rejected.
+                       $editInfo = $this->prepareContentForEdit( $content, $revision, $user, null, false );
+               }
+
                // Save it to the parser cache.
                // Make sure the cache time matches page_touched to avoid double parsing.
                ParserCache::singleton()->save(
@@ -2238,6 +2302,7 @@ class WikiPage implements Page, IDBAccessObject {
                                DeferredUpdates::addUpdate( $update );
                        }
                        if ( $wgRCWatchCategoryMembership
+                               && $this->getContentHandler()->supportsCategories() === true
                                && ( $options['changed'] || $options['created'] )
                                && !$options['restored']
                        ) {
@@ -2246,17 +2311,17 @@ class WikiPage implements Page, IDBAccessObject {
                                // bot/deletion/IP flags, ect.
                                JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
                                        $this->getTitle(),
-                                       array(
+                                       [
                                                'pageId' => $this->getId(),
                                                'revTimestamp' => $revision->getTimestamp()
-                                       )
+                                       ]
                                ) );
                        }
                }
 
-               Hooks::run( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
+               Hooks::run( 'ArticleEditUpdates', [ &$this, &$editInfo, $options['changed'] ] );
 
-               if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
+               if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$this ] ) ) {
                        // Flush old entries from the `recentchanges` table
                        if ( mt_rand( 0, 9 ) == 0 ) {
                                JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
@@ -2302,7 +2367,7 @@ class WikiPage implements Page, IDBAccessObject {
                        } else {
                                // Allow extensions to prevent user notification
                                // when a new message is added to their talk page
-                               if ( Hooks::run( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) {
+                               if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$this, $recipient ] ) ) {
                                        if ( User::isIP( $shortTitle ) ) {
                                                // An anonymous user
                                                $recipient->setNewtalk( true, $revision );
@@ -2323,6 +2388,10 @@ class WikiPage implements Page, IDBAccessObject {
                        }
 
                        MessageCache::singleton()->replace( $shortTitle, $msgtext );
+
+                       if ( $wgContLang->hasVariants() ) {
+                               $wgContLang->updateConversionTable( $this->mTitle );
+                       }
                }
 
                if ( $options['created'] ) {
@@ -2330,41 +2399,10 @@ class WikiPage implements Page, IDBAccessObject {
                } elseif ( $options['changed'] ) { // bug 50785
                        self::onArticleEdit( $this->mTitle, $revision );
                }
-       }
-
-       /**
-        * Edit an article without doing all that other stuff
-        * The article must already exist; link tables etc
-        * are not updated, caches are not flushed.
-        *
-        * @param Content $content Content submitted
-        * @param User $user The relevant user
-        * @param string $comment Comment submitted
-        * @param bool $minor Whereas it's a minor modification
-        * @param string $serialFormat Format for storing the content in the database
-        */
-       public function doQuickEditContent(
-               Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
-       ) {
-
-               $serialized = $content->serialize( $serialFormat );
-
-               $dbw = wfGetDB( DB_MASTER );
-               $revision = new Revision( array(
-                       'title'      => $this->getTitle(), // for determining the default content model
-                       'page'       => $this->getId(),
-                       'user_text'  => $user->getName(),
-                       'user'       => $user->getId(),
-                       'text'       => $serialized,
-                       'length'     => $content->getSize(),
-                       'comment'    => $comment,
-                       'minor_edit' => $minor ? 1 : 0,
-               ) ); // XXX: set the content object?
-               $revision->insertOn( $dbw );
-               $this->updateRevisionOn( $dbw, $revision );
-
-               Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
 
+               ResourceLoaderWikiModule::invalidateModuleCache(
+                       $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
+               );
        }
 
        /**
@@ -2376,10 +2414,13 @@ class WikiPage implements Page, IDBAccessObject {
         * @param int &$cascade Set to false if cascading protection isn't allowed.
         * @param string $reason
         * @param User $user The user updating the restrictions
-        * @return Status
+        * @param string|string[] $tags Change tags to add to the pages and protection log entries
+        *   ($user should be able to add the specified tags before this is called)
+        * @return Status Status object; if action is taken, $status->value is the log_id of the
+        *   protection log entry.
         */
        public function doUpdateRestrictions( array $limit, array $expiry,
-               &$cascade, $reason, User $user
+               &$cascade, $reason, User $user, $tags = null
        ) {
                global $wgCascadingRestrictionLevels, $wgContLang;
 
@@ -2457,18 +2498,21 @@ class WikiPage implements Page, IDBAccessObject {
                // Truncate for whole multibyte characters
                $reason = $wgContLang->truncate( $reason, 255 );
 
-               $logRelationsValues = array();
+               $logRelationsValues = [];
                $logRelationsField = null;
-               $logParamsDetails = array();
+               $logParamsDetails = [];
+
+               // Null revision (used for change tag insertion)
+               $nullRevision = null;
 
                if ( $id ) { // Protection of existing page
-                       if ( !Hooks::run( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
+                       if ( !Hooks::run( 'ArticleProtect', [ &$this, &$user, $limit, $reason ] ) ) {
                                return Status::newGood();
                        }
 
                        // Only certain restrictions can cascade...
                        $editrestriction = isset( $limit['edit'] )
-                               ? array( $limit['edit'] )
+                               ? [ $limit['edit'] ]
                                : $this->mTitle->getRestrictions( 'edit' );
                        foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
                                $editrestriction[$key] = 'editprotected'; // backwards compatibility
@@ -2511,55 +2555,55 @@ class WikiPage implements Page, IDBAccessObject {
                        foreach ( $limit as $action => $restrictions ) {
                                $dbw->delete(
                                        'page_restrictions',
-                                       array(
+                                       [
                                                'pr_page' => $id,
                                                'pr_type' => $action
-                                       ),
+                                       ],
                                        __METHOD__
                                );
                                if ( $restrictions != '' ) {
                                        $cascadeValue = ( $cascade && $action == 'edit' ) ? 1 : 0;
                                        $dbw->insert(
                                                'page_restrictions',
-                                               array(
+                                               [
                                                        'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
                                                        'pr_page' => $id,
                                                        'pr_type' => $action,
                                                        'pr_level' => $restrictions,
                                                        'pr_cascade' => $cascadeValue,
                                                        'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
-                                               ),
+                                               ],
                                                __METHOD__
                                        );
                                        $logRelationsValues[] = $dbw->insertId();
-                                       $logParamsDetails[] = array(
+                                       $logParamsDetails[] = [
                                                'type' => $action,
                                                'level' => $restrictions,
                                                'expiry' => $expiry[$action],
                                                'cascade' => (bool)$cascadeValue,
-                                       );
+                                       ];
                                }
                        }
 
                        // Clear out legacy restriction fields
                        $dbw->update(
                                'page',
-                               array( 'page_restrictions' => '' ),
-                               array( 'page_id' => $id ),
+                               [ 'page_restrictions' => '' ],
+                               [ 'page_id' => $id ],
                                __METHOD__
                        );
 
                        Hooks::run( 'NewRevisionFromEditComplete',
-                               array( $this, $nullRevision, $latest, $user ) );
-                       Hooks::run( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
+                               [ $this, $nullRevision, $latest, $user ] );
+                       Hooks::run( 'ArticleProtectComplete', [ &$this, &$user, $limit, $reason ] );
                } else { // Protection of non-existing page (also known as "title protection")
                        // Cascade protection is meaningless in this case
                        $cascade = false;
 
                        if ( $limit['create'] != '' ) {
                                $dbw->replace( 'protected_titles',
-                                       array( array( 'pt_namespace', 'pt_title' ) ),
-                                       array(
+                                       [ [ 'pt_namespace', 'pt_title' ] ],
+                                       [
                                                'pt_namespace' => $this->mTitle->getNamespace(),
                                                'pt_title' => $this->mTitle->getDBkey(),
                                                'pt_create_perm' => $limit['create'],
@@ -2567,19 +2611,19 @@ class WikiPage implements Page, IDBAccessObject {
                                                'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
                                                'pt_user' => $user->getId(),
                                                'pt_reason' => $reason,
-                                       ), __METHOD__
+                                       ], __METHOD__
                                );
-                               $logParamsDetails[] = array(
+                               $logParamsDetails[] = [
                                        'type' => 'create',
                                        'level' => $limit['create'],
                                        'expiry' => $expiry['create'],
-                               );
+                               ];
                        } else {
                                $dbw->delete( 'protected_titles',
-                                       array(
+                                       [
                                                'pt_namespace' => $this->mTitle->getNamespace(),
                                                'pt_title' => $this->mTitle->getDBkey()
-                                       ), __METHOD__
+                                       ], __METHOD__
                                );
                        }
                }
@@ -2588,14 +2632,14 @@ class WikiPage implements Page, IDBAccessObject {
                InfoAction::invalidateCache( $this->mTitle );
 
                if ( $logAction == 'unprotect' ) {
-                       $params = array();
+                       $params = [];
                } else {
                        $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
-                       $params = array(
+                       $params = [
                                '4::description' => $protectDescriptionLog, // parameter for IRC
                                '5:bool:cascade' => $cascade,
                                'details' => $logParamsDetails, // parameter for localize and api
-                       );
+                       ];
                }
 
                // Update the protection log
@@ -2604,13 +2648,17 @@ class WikiPage implements Page, IDBAccessObject {
                $logEntry->setComment( $reason );
                $logEntry->setPerformer( $user );
                $logEntry->setParameters( $params );
+               if ( !is_null( $nullRevision ) ) {
+                       $logEntry->setAssociatedRevId( $nullRevision->getId() );
+               }
+               $logEntry->setTags( $tags );
                if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
-                       $logEntry->setRelations( array( $logRelationsField => $logRelationsValues ) );
+                       $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
                }
                $logId = $logEntry->insert();
                $logEntry->publish( $logId );
 
-               return Status::newGood();
+               return Status::newGood( $logId );
        }
 
        /**
@@ -2694,14 +2742,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
@@ -2760,7 +2808,7 @@ class WikiPage implements Page, IDBAccessObject {
                        throw new MWException( __METHOD__ . ' given non-array restriction set' );
                }
 
-               $bits = array();
+               $bits = [];
                ksort( $limit );
 
                foreach ( array_filter( $limit ) as $action => $restrictions ) {
@@ -2806,12 +2854,14 @@ class WikiPage implements Page, IDBAccessObject {
         * @param bool $u2 Unused
         * @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
         * @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
         */
        public function doDeleteArticleReal(
-               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+               $tags = []
        ) {
                global $wgUser, $wgContentHandlerUseDB;
 
@@ -2827,7 +2877,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                $user = is_null( $user ) ? $wgUser : $user;
                if ( !Hooks::run( 'ArticleDelete',
-                       array( &$this, &$user, &$reason, &$error, &$status, $suppress )
+                       [ &$this, &$user, &$reason, &$error, &$status, $suppress ]
                ) ) {
                        if ( $status->isOK() ) {
                                // Hook aborted but didn't set a fatal status
@@ -2840,7 +2890,7 @@ class WikiPage implements Page, IDBAccessObject {
                $dbw->startAtomic( __METHOD__ );
 
                $this->loadPageData( self::READ_LATEST );
-               $id = $this->getID();
+               $id = $this->getId();
                // T98706: lock the page from various other updates but avoid using
                // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
                // the revisions queries (which also JOIN on user). Only lock the page
@@ -2854,84 +2904,90 @@ class WikiPage implements Page, IDBAccessObject {
                        return $status;
                }
 
+               // Given the lock above, we can be confident in the title and page ID values
+               $namespace = $this->getTitle()->getNamespace();
+               $dbKey = $this->getTitle()->getDBkey();
+
                // At this point we are now comitted to returning an OK
                // status unless some DB query error or other exception comes up.
                // This way callers don't have to call rollback() if $status is bad
                // unless they actually try to catch exceptions (which is rare).
 
                // we need to remember the old content so we can use it to generate all deletion updates.
-               $content = $this->getContent( Revision::RAW );
+               $revision = $this->getRevision();
+               try {
+                       $content = $this->getContent( Revision::RAW );
+               } catch ( Exception $ex ) {
+                       wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
+                               . $ex->getMessage() );
+
+                       $content = null;
+               }
+
+               $fields = Revision::selectFields();
+               $bitfield = false;
 
                // Bitfields to further suppress the content
                if ( $suppress ) {
-                       $bitfield = 0;
-                       // This should be 15...
-                       $bitfield |= Revision::DELETED_TEXT;
-                       $bitfield |= Revision::DELETED_COMMENT;
-                       $bitfield |= Revision::DELETED_USER;
-                       $bitfield |= Revision::DELETED_RESTRICTED;
-               } else {
-                       $bitfield = 'rev_deleted';
-               }
-
-               /**
-                * For now, shunt the revision data into the archive table.
-                * Text is *not* removed from the text table; bulk storage
-                * is left intact to avoid breaking block-compression or
-                * immutable storage schemes.
-                *
-                * For backwards compatibility, note that some older archive
-                * table entries will have ar_text and ar_flags fields still.
-                *
-                * In the future, we may keep revisions and mark them with
-                * the rev_deleted field, which is reserved for this purpose.
-                */
-
-               $row = array(
-                       'ar_namespace'  => 'page_namespace',
-                       'ar_title'      => 'page_title',
-                       'ar_comment'    => 'rev_comment',
-                       'ar_user'       => 'rev_user',
-                       'ar_user_text'  => 'rev_user_text',
-                       'ar_timestamp'  => 'rev_timestamp',
-                       'ar_minor_edit' => 'rev_minor_edit',
-                       'ar_rev_id'     => 'rev_id',
-                       'ar_parent_id'  => 'rev_parent_id',
-                       'ar_text_id'    => 'rev_text_id',
-                       'ar_text'       => '\'\'', // Be explicit to appease
-                       'ar_flags'      => '\'\'', // MySQL's "strict mode"...
-                       'ar_len'        => 'rev_len',
-                       'ar_page_id'    => 'page_id',
-                       'ar_deleted'    => $bitfield,
-                       'ar_sha1'       => 'rev_sha1',
+                       $bitfield = Revision::SUPPRESSED_ALL;
+                       $fields = array_diff( $fields, [ 'rev_deleted' ] );
+               }
+
+               // For now, shunt the revision data into the archive table.
+               // Text is *not* removed from the text table; bulk storage
+               // is left intact to avoid breaking block-compression or
+               // immutable storage schemes.
+               // In the future, we may keep revisions and mark them with
+               // the rev_deleted field, which is reserved for this purpose.
+
+               // Get all of the page revisions
+               $res = $dbw->select(
+                       'revision',
+                       $fields,
+                       [ 'rev_page' => $id ],
+                       __METHOD__,
+                       'FOR UPDATE'
                );
-
-               if ( $wgContentHandlerUseDB ) {
-                       $row['ar_content_model'] = 'rev_content_model';
-                       $row['ar_content_format'] = 'rev_content_format';
+               // Build their equivalent archive rows
+               $rowsInsert = [];
+               foreach ( $res as $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,
+                               'ar_minor_edit' => $row->rev_minor_edit,
+                               'ar_rev_id'     => $row->rev_id,
+                               'ar_parent_id'  => $row->rev_parent_id,
+                               'ar_text_id'    => $row->rev_text_id,
+                               'ar_text'       => '',
+                               'ar_flags'      => '',
+                               'ar_len'        => $row->rev_len,
+                               'ar_page_id'    => $id,
+                               'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
+                               'ar_sha1'       => $row->rev_sha1,
+                       ];
+                       if ( $wgContentHandlerUseDB ) {
+                               $rowInsert['ar_content_model'] = $row->rev_content_model;
+                               $rowInsert['ar_content_format'] = $row->rev_content_format;
+                       }
+                       $rowsInsert[] = $rowInsert;
                }
+               // Copy them into the archive table
+               $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
+               // Save this so we can pass it to the ArticleDeleteComplete hook.
+               $archivedRevisionCount = $dbw->affectedRows();
 
-               // Copy all the page revisions into the archive table
-               $dbw->insertSelect(
-                       'archive',
-                       array( 'page', 'revision' ),
-                       $row,
-                       array(
-                               'page_id' => $id,
-                               'page_id = rev_page'
-                       ),
-                       __METHOD__
-               );
+               // Clone the title and wikiPage, so we have the information we need when
+               // we log and run the ArticleDeleteComplete hook.
+               $logTitle = clone $this->mTitle;
+               $wikiPageBeforeDelete = clone $this;
 
                // Now that it's safely backed up, delete it
-               $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
-
-               if ( !$dbw->cascadingDeletes() ) {
-                       $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
-               }
-
-               // Clone the title, so we have the information we need when we log
-               $logTitle = clone $this->mTitle;
+               $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
+               $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
 
                // Log the deletion, if the page was suppressed, put it in the suppression log instead
                $logtype = $suppress ? 'suppress' : 'delete';
@@ -2940,19 +2996,30 @@ class WikiPage implements Page, IDBAccessObject {
                $logEntry->setPerformer( $user );
                $logEntry->setTarget( $logTitle );
                $logEntry->setComment( $reason );
+               $logEntry->setTags( $tags );
                $logid = $logEntry->insert();
 
-               $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
-                       // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
-                       $logEntry->publish( $logid );
-               } );
+               $dbw->onTransactionPreCommitOrIdle(
+                       function () use ( $dbw, $logEntry, $logid ) {
+                               // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
+                               $logEntry->publish( $logid );
+                       },
+                       __METHOD__
+               );
 
                $dbw->endAtomic( __METHOD__ );
 
-               $this->doDeleteUpdates( $id, $content );
-
-               Hooks::run( 'ArticleDeleteComplete',
-                       array( &$this, &$user, $reason, $id, $content, $logEntry ) );
+               $this->doDeleteUpdates( $id, $content, $revision );
+
+               Hooks::run( 'ArticleDeleteComplete', [
+                       &$wikiPageBeforeDelete,
+                       &$user,
+                       $reason,
+                       $id,
+                       $content,
+                       $logEntry,
+                       $archivedRevisionCount
+               ] );
                $status->value = $logid;
 
                // Show log excerpt on 404 pages rather than just a link
@@ -2973,15 +3040,15 @@ class WikiPage implements Page, IDBAccessObject {
                return (int)wfGetDB( DB_MASTER )->selectField(
                        'page',
                        'page_latest',
-                       array(
+                       [
                                'page_id' => $this->getId(),
                                // Typically page_id is enough, but some code might try to do
                                // updates assuming the title is the same, so verify that
                                'page_namespace' => $this->getTitle()->getNamespace(),
                                'page_title' => $this->getTitle()->getDBkey()
-                       ),
+                       ],
                        __METHOD__,
-                       array( 'FOR UPDATE' )
+                       [ 'FOR UPDATE' ]
                );
        }
 
@@ -2989,13 +3056,22 @@ class WikiPage implements Page, IDBAccessObject {
         * Do some database updates after deletion
         *
         * @param int $id The page_id value of the page being deleted
-        * @param Content $content Optional page content to be used when determining
+        * @param Content|null $content Optional page content to be used when determining
         *   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
         */
-       public function doDeleteUpdates( $id, Content $content = null ) {
+       public function doDeleteUpdates( $id, Content $content = null, Revision $revision = null ) {
+               try {
+                       $countable = $this->isCountable();
+               } catch ( Exception $ex ) {
+                       // fallback for deleting broken pages for which we cannot load the content for
+                       // some reason. Note that doDeleteArticleReal() already logged this problem.
+                       $countable = false;
+               }
+
                // Update site status
-               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
+               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$countable, -1 ) );
 
                // Delete pagelinks, update secondary indexes, etc
                $updates = $this->getDeletionUpdates( $content );
@@ -3013,6 +3089,9 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Clear caches
                WikiPage::onArticleDelete( $this->mTitle );
+               ResourceLoaderWikiModule::invalidateModuleCache(
+                       $this->mTitle, $revision, null, wfWikiID()
+               );
 
                // Reset this object and the Title object
                $this->loadFromRow( false, self::READ_LATEST );
@@ -3029,6 +3108,7 @@ class WikiPage implements Page, IDBAccessObject {
         * to do the dirty work
         *
         * @todo Separate the business/permission stuff out from backend code
+        * @todo Remove $token parameter. Already verified by RollbackAction and ApiRollback.
         *
         * @param string $fromP Name of the user whose edits to rollback.
         * @param string $summary Custom summary. Set to default summary if empty.
@@ -3040,13 +3120,17 @@ class WikiPage implements Page, IDBAccessObject {
         *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
         *
         * @param User $user The user performing the rollback
+        * @param array|null $tags Change tags to apply to the rollback
+        * Callers are responsible for permission checks
+        * (with ChangeTags::canAddTagsAccompanyingChange)
+        *
         * @return array Array of errors, each error formatted as
         *   array(messagekey, param1, param2, ...).
         * On success, the array is empty.  This array can also be passed to
         * OutputPage::showPermissionsErrorPage().
         */
        public function doRollback(
-               $fromP, $summary, $token, $bot, &$resultDetails, User $user
+               $fromP, $summary, $token, $bot, &$resultDetails, User $user, $tags = null
        ) {
                $resultDetails = null;
 
@@ -3055,12 +3139,12 @@ class WikiPage implements Page, IDBAccessObject {
                $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
                $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
 
-               if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
-                       $errors[] = array( 'sessionfailure' );
+               if ( !$user->matchEditToken( $token, 'rollback' ) ) {
+                       $errors[] = [ 'sessionfailure' ];
                }
 
                if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
-                       $errors[] = array( 'actionthrottledtext' );
+                       $errors[] = [ 'actionthrottledtext' ];
                }
 
                // If there were errors, bail out now
@@ -3068,7 +3152,7 @@ class WikiPage implements Page, IDBAccessObject {
                        return $errors;
                }
 
-               return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user );
+               return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user, $tags );
        }
 
        /**
@@ -3085,34 +3169,40 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @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
+        * (with ChangeTags::canAddTagsAccompanyingChange)
+        *
         * @return array
         */
-       public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) {
+       public function commitRollback( $fromP, $summary, $bot,
+               &$resultDetails, User $guser, $tags = null
+       ) {
                global $wgUseRCPatrol, $wgContLang;
 
                $dbw = wfGetDB( DB_MASTER );
 
                if ( wfReadOnly() ) {
-                       return array( array( 'readonlytext' ) );
+                       return [ [ 'readonlytext' ] ];
                }
 
                // Get the last editor
                $current = $this->getRevision();
                if ( is_null( $current ) ) {
                        // Something wrong... no page?
-                       return array( array( 'notanarticle' ) );
+                       return [ [ 'notanarticle' ] ];
                }
 
                $from = str_replace( '_', ' ', $fromP );
                // User name given should match up with the top revision.
                // If the user was deleted then $from should be empty.
                if ( $from != $current->getUserText() ) {
-                       $resultDetails = array( 'current' => $current );
-                       return array( array( 'alreadyrolled',
+                       $resultDetails = [ 'current' => $current ];
+                       return [ [ 'alreadyrolled',
                                htmlspecialchars( $this->mTitle->getPrefixedText() ),
                                htmlspecialchars( $fromP ),
                                htmlspecialchars( $current->getUserText() )
-                       ) );
+                       ] ];
                }
 
                // Get the last edit not by this person...
@@ -3120,21 +3210,21 @@ class WikiPage implements Page, IDBAccessObject {
                $user = intval( $current->getUser( Revision::RAW ) );
                $user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) );
                $s = $dbw->selectRow( 'revision',
-                       array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
-                       array( 'rev_page' => $current->getPage(),
+                       [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
+                       [ 'rev_page' => $current->getPage(),
                                "rev_user != {$user} OR rev_user_text != {$user_text}"
-                       ), __METHOD__,
-                       array( 'USE INDEX' => 'page_timestamp',
-                               'ORDER BY' => 'rev_timestamp DESC' )
+                       ], __METHOD__,
+                       [ 'USE INDEX' => 'page_timestamp',
+                               'ORDER BY' => 'rev_timestamp DESC' ]
                        );
                if ( $s === false ) {
                        // No one else ever edited this page
-                       return array( array( 'cantrollback' ) );
+                       return [ [ 'cantrollback' ] ];
                } elseif ( $s->rev_deleted & Revision::DELETED_TEXT
                        || $s->rev_deleted & Revision::DELETED_USER
                ) {
                        // Only admins can see this text
-                       return array( array( 'notvisiblerev' ) );
+                       return [ [ 'notvisiblerev' ] ];
                }
 
                // Generate the edit summary if necessary
@@ -3148,11 +3238,11 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                // Allow the custom summary to use the same args as the default message
-               $args = array(
+               $args = [
                        $target->getUserText(), $from, $s->rev_id,
                        $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
                        $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
-               );
+               ];
                if ( $summary instanceof Message ) {
                        $summary = $summary->params( $args )->inContentLanguage()->text();
                } else {
@@ -3166,7 +3256,7 @@ class WikiPage implements Page, IDBAccessObject {
                $summary = $wgContLang->truncate( $summary, 255 );
 
                // Save
-               $flags = EDIT_UPDATE;
+               $flags = EDIT_UPDATE | EDIT_INTERNAL;
 
                if ( $guser->isAllowed( 'minoredit' ) ) {
                        $flags |= EDIT_MINOR;
@@ -3176,18 +3266,23 @@ 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(),
-                       $guser
+                       $guser,
+                       null,
+                       $tags
                );
 
                // 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).
-               $set = array();
+               $set = [];
                if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
                        // Mark all reverted edits as bot
                        $set['rc_bot'] = 1;
@@ -3200,11 +3295,11 @@ class WikiPage implements Page, IDBAccessObject {
 
                if ( count( $set ) ) {
                        $dbw->update( 'recentchanges', $set,
-                               array( /* WHERE */
+                               [ /* WHERE */
                                        'rc_cur_id' => $current->getPage(),
                                        'rc_user_text' => $current->getUserText(),
                                        'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
-                               ),
+                               ],
                                __METHOD__
                        );
                }
@@ -3218,26 +3313,42 @@ class WikiPage implements Page, IDBAccessObject {
                        ? $status->value['revision']
                        : null;
                if ( !( $statusRev instanceof Revision ) ) {
-                       $resultDetails = array( 'current' => $current );
-                       return array( array( 'alreadyrolled',
+                       $resultDetails = [ 'current' => $current ];
+                       return [ [ 'alreadyrolled',
                                        htmlspecialchars( $this->mTitle->getPrefixedText() ),
                                        htmlspecialchars( $fromP ),
                                        htmlspecialchars( $current->getUserText() )
-                       ) );
+                       ] ];
+               }
+
+               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', array( $this, $guser, $target, $current ) );
+               Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
 
-               $resultDetails = array(
+               $resultDetails = [
                        'summary' => $summary,
                        'current' => $current,
                        'target' => $target,
                        'newid' => $revId
-               );
+               ];
 
-               return array();
+               return [];
        }
 
        /**
@@ -3260,6 +3371,16 @@ class WikiPage implements Page, IDBAccessObject {
                $title->touchLinks();
                $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 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();
+               }
        }
 
        /**
@@ -3268,6 +3389,8 @@ class WikiPage implements Page, IDBAccessObject {
         * @param Title $title
         */
        public static function onArticleDelete( Title $title ) {
+               global $wgContLang;
+
                // Update existence markers on article/talk tabs...
                $other = $title->getOtherPage();
 
@@ -3276,6 +3399,8 @@ class WikiPage implements Page, IDBAccessObject {
                $title->touchLinks();
                $title->purgeSquid();
 
+               MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
                // File cache
                HTMLFileCache::clearFileCache( $title );
                InfoAction::invalidateCache( $title );
@@ -3283,6 +3408,10 @@ class WikiPage implements Page, IDBAccessObject {
                // Messages
                if ( $title->getNamespace() == NS_MEDIAWIKI ) {
                        MessageCache::singleton()->replace( $title->getDBkey(), false );
+
+                       if ( $wgContLang->hasVariants() ) {
+                               $wgContLang->updateConversionTable( $title );
+                       }
                }
 
                // Images
@@ -3315,7 +3444,9 @@ class WikiPage implements Page, IDBAccessObject {
                // Invalidate the caches of all pages which redirect here
                DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) );
 
-               // Purge squid for this page only
+               MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $title );
+
+               // Purge CDN for this page only
                $title->purgeSquid();
                // Clear file cache for this page only
                HTMLFileCache::clearFileCache( $title );
@@ -3337,15 +3468,15 @@ class WikiPage implements Page, IDBAccessObject {
        public function getCategories() {
                $id = $this->getId();
                if ( $id == 0 ) {
-                       return TitleArray::newFromResult( new FakeResultWrapper( array() ) );
+                       return TitleArray::newFromResult( new FakeResultWrapper( [] ) );
                }
 
-               $dbr = wfGetDB( DB_SLAVE );
+               $dbr = wfGetDB( DB_REPLICA );
                $res = $dbr->select( 'categorylinks',
-                       array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ),
-                       // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
+                       [ 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ],
+                       // Have to do that since Database::fieldNamesWithAlias treats numeric indexes
                        // as not being aliases, and NS_CATEGORY is numeric
-                       array( 'cl_from' => $id ),
+                       [ 'cl_from' => $id ],
                        __METHOD__ );
 
                return TitleArray::newFromResult( $res );
@@ -3358,18 +3489,18 @@ class WikiPage implements Page, IDBAccessObject {
         * @return array Array of Title objects
         */
        public function getHiddenCategories() {
-               $result = array();
+               $result = [];
                $id = $this->getId();
 
                if ( $id == 0 ) {
-                       return array();
+                       return [];
                }
 
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
-                       array( 'cl_to' ),
-                       array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
-                               'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ),
+               $dbr = wfGetDB( DB_REPLICA );
+               $res = $dbr->select( [ 'categorylinks', 'page_props', 'page' ],
+                       [ 'cl_to' ],
+                       [ 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
+                               'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ],
                        __METHOD__ );
 
                if ( $res !== false ) {
@@ -3394,7 +3525,7 @@ class WikiPage implements Page, IDBAccessObject {
                // NOTE: stub for backwards-compatibility. assumes the given text is
                // wikitext. will break horribly if it isn't.
 
-               ContentHandler::deprecated( __METHOD__, '1.21' );
+               wfDeprecated( __METHOD__, '1.21' );
 
                $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
                $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
@@ -3418,90 +3549,103 @@ class WikiPage implements Page, IDBAccessObject {
         * Update all the appropriate counts in the category table, given that
         * we've added the categories $added and deleted the categories $deleted.
         *
+        * This should only be called from deferred updates or jobs to avoid contention.
+        *
         * @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)
         */
-       public function updateCategoryCounts( array $added, array $deleted ) {
-               $that = $this;
-               $method = __METHOD__;
-               $dbw = wfGetDB( DB_MASTER );
+       public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
+               $id = $id ?: $this->getId();
+               $ns = $this->getTitle()->getNamespace();
 
-               // Do this at the end of the commit to reduce lock wait timeouts
-               $dbw->onTransactionPreCommitOrIdle(
-                       function () use ( $dbw, $that, $method, $added, $deleted ) {
-                               $ns = $that->getTitle()->getNamespace();
-
-                               $addFields = array( 'cat_pages = cat_pages + 1' );
-                               $removeFields = array( 'cat_pages = cat_pages - 1' );
-                               if ( $ns == NS_CATEGORY ) {
-                                       $addFields[] = 'cat_subcats = cat_subcats + 1';
-                                       $removeFields[] = 'cat_subcats = cat_subcats - 1';
-                               } elseif ( $ns == NS_FILE ) {
-                                       $addFields[] = 'cat_files = cat_files + 1';
-                                       $removeFields[] = 'cat_files = cat_files - 1';
-                               }
+               $addFields = [ 'cat_pages = cat_pages + 1' ];
+               $removeFields = [ 'cat_pages = cat_pages - 1' ];
+               if ( $ns == NS_CATEGORY ) {
+                       $addFields[] = 'cat_subcats = cat_subcats + 1';
+                       $removeFields[] = 'cat_subcats = cat_subcats - 1';
+               } elseif ( $ns == NS_FILE ) {
+                       $addFields[] = 'cat_files = cat_files + 1';
+                       $removeFields[] = 'cat_files = cat_files - 1';
+               }
 
-                               if ( count( $added ) ) {
-                                       $existingAdded = $dbw->selectFieldValues(
-                                               'category',
-                                               'cat_title',
-                                               array( 'cat_title' => $added ),
-                                               __METHOD__
-                                       );
+               $dbw = wfGetDB( DB_MASTER );
 
-                                       // For category rows that already exist, do a plain
-                                       // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
-                                       // to avoid creating gaps in the cat_id sequence.
-                                       if ( count( $existingAdded ) ) {
-                                               $dbw->update(
-                                                       'category',
-                                                       $addFields,
-                                                       array( 'cat_title' => $existingAdded ),
-                                                       __METHOD__
-                                               );
-                                       }
+               if ( count( $added ) ) {
+                       $existingAdded = $dbw->selectFieldValues(
+                               'category',
+                               'cat_title',
+                               [ 'cat_title' => $added ],
+                               __METHOD__
+                       );
 
-                                       $missingAdded = array_diff( $added, $existingAdded );
-                                       if ( count( $missingAdded ) ) {
-                                               $insertRows = array();
-                                               foreach ( $missingAdded as $cat ) {
-                                                       $insertRows[] = array(
-                                                               'cat_title'   => $cat,
-                                                               'cat_pages'   => 1,
-                                                               'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
-                                                               'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
-                                                       );
-                                               }
-                                               $dbw->upsert(
-                                                       'category',
-                                                       $insertRows,
-                                                       array( 'cat_title' ),
-                                                       $addFields,
-                                                       $method
-                                               );
-                                       }
-                               }
+                       // For category rows that already exist, do a plain
+                       // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
+                       // to avoid creating gaps in the cat_id sequence.
+                       if ( count( $existingAdded ) ) {
+                               $dbw->update(
+                                       'category',
+                                       $addFields,
+                                       [ 'cat_title' => $existingAdded ],
+                                       __METHOD__
+                               );
+                       }
 
-                               if ( count( $deleted ) ) {
-                                       $dbw->update(
-                                               'category',
-                                               $removeFields,
-                                               array( 'cat_title' => $deleted ),
-                                               $method
-                                       );
+                       $missingAdded = array_diff( $added, $existingAdded );
+                       if ( count( $missingAdded ) ) {
+                               $insertRows = [];
+                               foreach ( $missingAdded as $cat ) {
+                                       $insertRows[] = [
+                                               'cat_title'   => $cat,
+                                               'cat_pages'   => 1,
+                                               'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
+                                               'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
+                                       ];
                                }
+                               $dbw->upsert(
+                                       'category',
+                                       $insertRows,
+                                       [ 'cat_title' ],
+                                       $addFields,
+                                       __METHOD__
+                               );
+                       }
+               }
 
-                               foreach ( $added as $catName ) {
-                                       $cat = Category::newFromName( $catName );
-                                       Hooks::run( 'CategoryAfterPageAdded', array( $cat, $that ) );
-                               }
+               if ( count( $deleted ) ) {
+                       $dbw->update(
+                               'category',
+                               $removeFields,
+                               [ 'cat_title' => $deleted ],
+                               __METHOD__
+                       );
+               }
 
-                               foreach ( $deleted as $catName ) {
-                                       $cat = Category::newFromName( $catName );
-                                       Hooks::run( 'CategoryAfterPageRemoved', array( $cat, $that ) );
-                               }
+               foreach ( $added as $catName ) {
+                       $cat = Category::newFromName( $catName );
+                       Hooks::run( 'CategoryAfterPageAdded', [ $cat, $this ] );
+               }
+
+               foreach ( $deleted as $catName ) {
+                       $cat = Category::newFromName( $catName );
+                       Hooks::run( 'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
+               }
+
+               // Refresh counts on categories that should be empty now, to
+               // trigger possible deletion. Check master for the most
+               // up-to-date cat_pages.
+               if ( count( $deleted ) ) {
+                       $rows = $dbw->select(
+                               'category',
+                               [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
+                               [ 'cat_title' => $deleted, 'cat_pages <= 0' ],
+                               __METHOD__
+                       );
+                       foreach ( $rows as $row ) {
+                               $cat = Category::newFromRow( $row );
+                               $cat->refreshCounts();
                        }
-               );
+               }
        }
 
        /**
@@ -3516,22 +3660,24 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                if ( !Hooks::run( 'OpportunisticLinksUpdate',
-                       array( $this, $this->mTitle, $parserOutput )
+                       [ $this, $this->mTitle, $parserOutput ]
                ) ) {
                        return;
                }
 
-               $params = array(
+               $config = RequestContext::getMain()->getConfig();
+
+               $params = [
                        'isOpportunistic' => true,
                        'rootJobTimestamp' => $parserOutput->getCacheTime()
-               );
+               ];
 
                if ( $this->mTitle->areRestrictionsCascading() ) {
                        // If the page is cascade protecting, the links should really be up-to-date
                        JobQueueGroup::singleton()->lazyPush(
                                RefreshLinksJob::newPrioritized( $this->mTitle, $params )
                        );
-               } elseif ( $parserOutput->hasDynamicContent() ) {
+               } elseif ( !$config->get( 'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
                        // Assume the output contains "dynamic" time/random based magic words.
                        // Only update pages that expired due to dynamic content and NOT due to edits
                        // to referenced templates/files. When the cache expires due to dynamic content,
@@ -3543,7 +3689,8 @@ class WikiPage implements Page, IDBAccessObject {
                                // Although it would be de-duplicated, it would still waste I/O.
                                $cache = ObjectCache::getLocalClusterInstance();
                                $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
-                               if ( $cache->add( $key, time(), 60 ) ) {
+                               $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
+                               if ( $cache->add( $key, time(), $ttl ) ) {
                                        JobQueueGroup::singleton()->lazyPush(
                                                RefreshLinksJob::newDynamic( $this->mTitle, $params )
                                        );
@@ -3552,64 +3699,6 @@ class WikiPage implements Page, IDBAccessObject {
                }
        }
 
-       /**
-        * Return a list of templates used by this article.
-        * Uses the templatelinks table
-        *
-        * @deprecated since 1.19; use Title::getTemplateLinksFrom()
-        * @return array Array of Title objects
-        */
-       public function getUsedTemplates() {
-               return $this->mTitle->getTemplateLinksFrom();
-       }
-
-       /**
-        * This function is called right before saving the wikitext,
-        * so we can do things like signatures and links-in-context.
-        *
-        * @deprecated since 1.19; use Parser::preSaveTransform() instead
-        * @param string $text Article contents
-        * @param User $user User doing the edit
-        * @param ParserOptions $popts Parser options, default options for
-        *   the user loaded if null given
-        * @return string Article contents with altered wikitext markup (signatures
-        *      converted, {{subst:}}, templates, etc.)
-        */
-       public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) {
-               global $wgParser, $wgUser;
-
-               wfDeprecated( __METHOD__, '1.19' );
-
-               $user = is_null( $user ) ? $wgUser : $user;
-
-               if ( $popts === null ) {
-                       $popts = ParserOptions::newFromUser( $user );
-               }
-
-               return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
-       }
-
-       /**
-        * Update the article's restriction field, and leave a log entry.
-        *
-        * @deprecated since 1.19
-        * @param array $limit Set of restriction keys
-        * @param string $reason
-        * @param int &$cascade Set to false if cascading protection isn't allowed.
-        * @param array $expiry Per restriction type expiration
-        * @param User $user The user updating the restrictions
-        * @return bool True on success
-        */
-       public function updateRestrictions(
-               $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null
-       ) {
-               global $wgUser;
-
-               $user = is_null( $user ) ? $wgUser : $user;
-
-               return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK();
-       }
-
        /**
         * Returns a list of updates to be performed when this page is deleted. The
         * updates should remove any information about this page from secondary data
@@ -3617,22 +3706,40 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @param Content|null $content Optional Content object for determining the
         *   necessary updates.
-        * @return DataUpdate[]
+        * @return DeferrableUpdate[]
         */
        public function getDeletionUpdates( Content $content = null ) {
                if ( !$content ) {
                        // load content object, which may be used to determine the necessary updates.
                        // XXX: the content may not be needed to determine the updates.
-                       $content = $this->getContent( Revision::RAW );
+                       try {
+                               $content = $this->getContent( Revision::RAW );
+                       } catch ( Exception $ex ) {
+                               // If we can't load the content, something is wrong. Perhaps that's why
+                               // the user is trying to delete the page, so let's not fail in that case.
+                               // Note that doDeleteArticleReal() will already have logged an issue with
+                               // loading the content.
+                       }
                }
 
                if ( !$content ) {
-                       $updates = array();
+                       $updates = [];
                } else {
                        $updates = $content->getDeletionUpdates( $this );
                }
 
-               Hooks::run( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
+               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;
+       }
 }