Merge "Avoid deprecated LinkCache::singleton()"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 14 Jun 2018 23:48:54 +0000 (23:48 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 14 Jun 2018 23:48:54 +0000 (23:48 +0000)
1  2 
includes/OutputPage.php
includes/Title.php
includes/page/WikiPage.php
includes/parser/CoreParserFunctions.php
includes/parser/Parser.php
tests/phpunit/MediaWikiTestCase.php

diff --combined includes/OutputPage.php
@@@ -755,7 -755,11 +755,7 @@@ class OutputPage extends ContextSource 
         * @return mixed Property value or null if not found
         */
        public function getProperty( $name ) {
 -              if ( isset( $this->mProperties[$name] ) ) {
 -                      return $this->mProperties[$name];
 -              } else {
 -                      return null;
 -              }
 +              return $this->mProperties[$name] ?? null;
        }
  
        /**
                );
  
                # Add the results to the link cache
-               $lb->addResultToCache( LinkCache::singleton(), $res );
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+               $lb->addResultToCache( $linkCache, $res );
  
                return $res;
        }
  
                // Pre-process information
                $separatorTransTable = $lang->separatorTransformTable();
 -              $separatorTransTable = $separatorTransTable ? $separatorTransTable : [];
 +              $separatorTransTable = $separatorTransTable ?: [];
                $compactSeparatorTransTable = [
                        implode( "\t", array_keys( $separatorTransTable ) ),
                        implode( "\t", $separatorTransTable ),
                ];
                $digitTransTable = $lang->digitTransformTable();
 -              $digitTransTable = $digitTransTable ? $digitTransTable : [];
 +              $digitTransTable = $digitTransTable ?: [];
                $compactDigitTransTable = [
                        implode( "\t", array_keys( $digitTransTable ) ),
                        implode( "\t", $digitTransTable ),
diff --combined includes/Title.php
@@@ -979,7 -979,7 +979,7 @@@ class Title implements LinkTarget 
                        && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
                        && $this->getArticleID( $flags )
                ) {
-                       $linkCache = LinkCache::singleton();
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        $linkCache->addLinkObj( $this ); # in case we already had an article ID
                        $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
                }
                        return $errors;
                }
  
 -              if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
 +              if ( $wgEmailConfirmToEdit
 +                      && !$user->isEmailConfirmed()
 +                      && $action === 'edit'
 +              ) {
                        $errors[] = [ 'confirmedittext' ];
                }
  
                        $this->mArticleID = 0;
                        return $this->mArticleID;
                }
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                if ( $flags & self::GAID_FOR_UPDATE ) {
                        $oldUpdate = $linkCache->forUpdate( true );
                        $linkCache->clearLink( $this );
                        return $this->mRedirect;
                }
  
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
                if ( $cached === null ) {
                        $this->mLength = 0;
                        return $this->mLength;
                }
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
                if ( $cached === null ) {
                        $this->mLatestID = 0;
                        return $this->mLatestID;
                }
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
                if ( $cached === null ) {
         * @param int $newid The new Article ID
         */
        public function resetArticleID( $newid ) {
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->clearLink( $this );
  
                if ( $newid === false ) {
        }
  
        public static function clearCaches() {
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->clear();
  
                $titleCache = self::getTitleCache();
  
                $retVal = [];
                if ( $res->numRows() ) {
-                       $linkCache = LinkCache::singleton();
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        foreach ( $res as $row ) {
                                $titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
                                if ( $titleObj ) {
                );
  
                $retVal = [];
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                foreach ( $res as $row ) {
                        if ( $row->page_id ) {
                                $titleObj = self::newFromRow( $row );
                // check, if the page language could be saved in the database, and if so and
                // the value is not requested already, lookup the page language using LinkCache
                if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
-                       $linkCache = LinkCache::singleton();
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        $linkCache->addLinkObj( $this );
                        $this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
                }
  use MediaWiki\Edit\PreparedEdit;
  use MediaWiki\Logger\LoggerFactory;
  use MediaWiki\MediaWikiServices;
 +use MediaWiki\Storage\DerivedPageDataUpdater;
 +use MediaWiki\Storage\PageUpdater;
 +use MediaWiki\Storage\RevisionRecord;
 +use MediaWiki\Storage\RevisionSlotsUpdate;
 +use MediaWiki\Storage\RevisionStore;
  use Wikimedia\Assert\Assert;
  use Wikimedia\Rdbms\FakeResultWrapper;
  use Wikimedia\Rdbms\IDatabase;
 -use Wikimedia\Rdbms\DBUnexpectedError;
 +use Wikimedia\Rdbms\LoadBalancer;
  
  /**
   * Class representing a MediaWiki article and history.
@@@ -93,11 -88,6 +93,11 @@@ class WikiPage implements Page, IDBAcce
         */
        protected $mLinksUpdated = '19700101000000';
  
 +      /**
 +       * @var DerivedPageDataUpdater|null
 +       */
 +      private $derivedDataUpdater = null;
 +
        /**
         * Constructor and clear the article
         * @param Title $title Reference to a Title object.
                }
        }
  
 +      /**
 +       * @return RevisionStore
 +       */
 +      private function getRevisionStore() {
 +              return MediaWikiServices::getInstance()->getRevisionStore();
 +      }
 +
 +      /**
 +       * @return ParserCache
 +       */
 +      private function getParserCache() {
 +              return MediaWikiServices::getInstance()->getParserCache();
 +      }
 +
 +      /**
 +       * @return LoadBalancer
 +       */
 +      private function getDBLoadBalancer() {
 +              return MediaWikiServices::getInstance()->getDBLoadBalancer();
 +      }
 +
        /**
         * @todo Move this UI stuff somewhere else
         *
                $this->mTimestamp = '';
                $this->mIsRedirect = false;
                $this->mLatest = false;
 -              // T59026: do not clear mPreparedEdit since prepareTextForEdit() already checks
 -              // the requested rev ID and content against the cached one for equality. For most
 +              // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
 +              // checks the requested rev ID and content against the cached one. For most
                // content types, the output should not change during the lifetime of this cache.
                // Clearing it can cause extra parses on edit for no reason.
        }
  
                if ( is_int( $from ) ) {
                        list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
 -                      $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
 +                      $loadBalancer = $this->getDBLoadBalancer();
                        $db = $loadBalancer->getConnection( $index );
                        $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
  
                $this->loadFromRow( $data, $from );
        }
  
 +      /**
 +       * Checks whether the page data was loaded using the given database access mode (or better).
 +       *
 +       * @since 1.32
 +       *
 +       * @param string|int $from One of the following:
 +       *   - "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.
 +       *
 +       * @return bool
 +       */
 +      public function wasLoadedFrom( $from ) {
 +              $from = self::convertSelectType( $from );
 +
 +              if ( !is_int( $from ) ) {
 +                      // No idea from where the caller got this data, assume replica DB.
 +                      $from = self::READ_NORMAL;
 +              }
 +
 +              if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
 +                      return true;
 +              }
 +
 +              return false;
 +      }
 +
        /**
         * Load the object from a database row
         *
         *          the master DB using SELECT FOR UPDATE
         */
        public function loadFromRow( $data, $from ) {
-               $lc = LinkCache::singleton();
+               $lc = MediaWikiServices::getInstance()->getLinkCache();
                $lc->clearLink( $this->mTitle );
  
                if ( $data ) {
        public function isCountable( $editInfo = false ) {
                global $wgArticleCountMethod;
  
 +              // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
 +
                if ( !$this->mTitle->isContentPage() ) {
                        return false;
                }
  
                if ( $editInfo ) {
 +                      // NOTE: only the main slot can make a page a redirect
                        $content = $editInfo->pstContent;
                } else {
                        $content = $this->getContent();
         * @return UserArrayFromResult
         */
        public function getContributors() {
 -              // @todo FIXME: This is expensive; cache this info somewhere.
 +              // @todo: This is expensive; cache this info somewhere.
  
                $dbr = wfGetDB( DB_REPLICA );
  
                }
  
                if ( $useParserCache ) {
 -                      $parserOutput = MediaWikiServices::getInstance()->getParserCache()
 +                      $parserOutput = $this->getParserCache()
                                ->get( $this, $parserOptions );
                        if ( $parserOutput !== false ) {
                                return $parserOutput;
         * or else the record will be left in a funky state.
         * Best if all done inside a transaction.
         *
 +       * @todo Factor out into a PageStore service, to be used by PageUpdater.
 +       *
         * @param IDatabase $dbw
         * @param int|null $pageId Custom page ID that will be used for the insert statement
         *
        /**
         * Update the page record to point to a newly saved revision.
         *
 +       * @todo Factor out into a PageStore service, or move into PageUpdater.
 +       *
         * @param IDatabase $dbw
         * @param Revision $revision For ID number, and text used to set
         *   length and redirect status fields
        ) {
                global $wgContentHandlerUseDB;
  
 +              // TODO: move into PageUpdater or PageStore
 +              // NOTE: when doing that, make sure cached fields get reset in doEditContent,
 +              // and in the compat stub!
 +
                // Assertion to try to catch T92046
                if ( (int)$revision->getId() === 0 ) {
                        throw new InvalidArgumentException(
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        // Update the LinkCache.
-                       LinkCache::singleton()->addGoodLinkObj(
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+                       $linkCache->addGoodLinkObj(
                                $this->getId(),
                                $this->mTitle,
                                $len,
        ) {
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
 -                      $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
 +                      $lb = $this->getDBLoadBalancer();
                        $dbr = $lb->getConnection( DB_REPLICA );
                        $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
                        // Try the master if this thread may have just added it.
  
        /**
         * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
 +       *
 +       * @deprecated since 1.32, use exists() instead, or simply omit the EDIT_UPDATE
 +       * and EDIT_NEW flags. To protect against race conditions, use PageUpdater::grabParentRevision.
 +       *
         * @param int $flags
         * @return int Updated $flags
         */
                return $flags;
        }
  
 +      /**
 +       * @return DerivedPageDataUpdater
 +       */
 +      private function newDerivedDataUpdater() {
 +              global $wgContLang, $wgRCWatchCategoryMembership, $wgArticleCountMethod;
 +
 +              $derivedDataUpdater = new DerivedPageDataUpdater(
 +                      $this, // NOTE: eventually, PageUpdater should not know about WikiPage
 +                      $this->getRevisionStore(),
 +                      $this->getParserCache(),
 +                      JobQueueGroup::singleton(),
 +                      MessageCache::singleton(),
 +                      $wgContLang,
 +                      LoggerFactory::getInstance( 'SaveParse' )
 +              );
 +
 +              $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
 +              $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
 +
 +              return $derivedDataUpdater;
 +      }
 +
 +      /**
 +       * Returns a DerivedPageDataUpdater for use with the given target revision or new content.
 +       * This method attempts to re-use the same DerivedPageDataUpdater instance for subsequent calls.
 +       * The parameters passed to this method are used to ensure that the DerivedPageDataUpdater
 +       * returned matches that caller's expectations, allowing an existing instance to be re-used
 +       * if the given parameters match that instance's internal state according to
 +       * DerivedPageDataUpdater::isReusableFor(), and creating a new instance of the parameters do not
 +       * match the existign one.
 +       *
 +       * If neither $forRevision nor $forUpdate is given, a new DerivedPageDataUpdater is always
 +       * created, replacing any DerivedPageDataUpdater currently cached.
 +       *
 +       * MCR migration note: this replaces WikiPage::prepareContentForEdit.
 +       *
 +       * @since 1.32
 +       *
 +       * @param User|null $forUser The user that will be used for, or was used for, PST.
 +       * @param RevisionRecord|null $forRevision The revision created by the edit for which
 +       *        to perform updates, if the edit was already saved.
 +       * @param RevisionSlotsUpdate|null $forUpdate The new content to be saved by the edit (pre PST),
 +       *        if the edit was not yet saved.
 +       *
 +       * @return DerivedPageDataUpdater
 +       */
 +      private function getDerivedDataUpdater(
 +              User $forUser = null,
 +              RevisionRecord $forRevision = null,
 +              RevisionSlotsUpdate $forUpdate = null
 +      ) {
 +              if ( !$forRevision && !$forUpdate ) {
 +                      // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
 +                      // going to use it with.
 +                      $this->derivedDataUpdater = null;
 +              }
 +
 +              if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
 +                      // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
 +                      // to it did not yet initialize it, because we don't know what data it will be
 +                      // initialized with.
 +                      $this->derivedDataUpdater = null;
 +              }
 +
 +              // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
 +              // However, there is no good way to construct a cache key. We'd need to check against all
 +              // cached instances.
 +
 +              if ( $this->derivedDataUpdater
 +                      && !$this->derivedDataUpdater->isReusableFor(
 +                              $forUser,
 +                              $forRevision,
 +                              $forUpdate
 +                      )
 +              ) {
 +                      $this->derivedDataUpdater = null;
 +              }
 +
 +              if ( !$this->derivedDataUpdater ) {
 +                      $this->derivedDataUpdater = $this->newDerivedDataUpdater();
 +              }
 +
 +              return $this->derivedDataUpdater;
 +      }
 +
 +      /**
 +       * Returns a PageUpdater for creating new revisions on this page (or creating the page).
 +       *
 +       * The PageUpdater can also be used to detect the need for edit conflict resolution,
 +       * and to protected such conflict resolution from concurrent edits using a check-and-set
 +       * mechanism.
 +       *
 +       * @since 1.32
 +       *
 +       * @param User $user
 +       *
 +       * @return PageUpdater
 +       */
 +      public function newPageUpdater( User $user ) {
 +              global $wgAjaxEditStash, $wgUseAutomaticEditSummaries, $wgPageCreationLog;
 +
 +              $pageUpdater = new PageUpdater(
 +                      $user,
 +                      $this, // NOTE: eventually, PageUpdater should not know about WikiPage
 +                      $this->getDerivedDataUpdater( $user ),
 +                      $this->getDBLoadBalancer(),
 +                      $this->getRevisionStore()
 +              );
 +
 +              $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
 +              $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
 +              $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
 +
 +              return $pageUpdater;
 +      }
 +
        /**
         * Change an existing article or create a new article. Updates RC and all necessary caches,
         * optionally via the deferred update array.
         *
 +       * @deprecated since 1.32, use PageUpdater::saveRevision instead. Note that the new method
 +       * expects callers to take care of checking EDIT_MINOR against the minoredit right, and to
 +       * apply the autopatrol right as appropriate.
 +       *
         * @param Content $content New content
 -       * @param string $summary Edit summary
 +       * @param string|CommentStoreComment $summary Edit summary
         * @param int $flags Bitfield:
         *      EDIT_NEW
         *          Article is known or assumed to be non-existent, create a new one
         *   This is not the parent revision ID, rather the revision ID for older
         *   content used as the source for a rollback, for example.
         * @param User $user The user doing the edit
 -       * @param string $serialFormat Format for storing the content in the
 -       *   database.
 +       * @param string $serialFormat IGNORED.
         * @param array|null $tags Change tags to apply to this edit
         * Callers are responsible for permission checks
         * (with ChangeTags::canAddTagsAccompanyingChange)
                Content $content, $summary, $flags = 0, $baseRevId = false,
                User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
        ) {
 -              global $wgUser, $wgUseAutomaticEditSummaries;
 +              global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
  
 -              // Old default parameter for $tags was null
 -              if ( $tags === null ) {
 -                      $tags = [];
 +              if ( !( $summary instanceof CommentStoreComment ) ) {
 +                      $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
                }
  
 -              // Low-level sanity check
 -              if ( $this->mTitle->getText() === '' ) {
 -                      throw new MWException( 'Something is trying to edit an article with an empty title' );
 -              }
 -              // Make sure the given content type is allowed for this page
 -              if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
 -                      return Status::newFatal( 'content-not-allowed-here',
 -                              ContentHandler::getLocalizedName( $content->getModel() ),
 -                              $this->mTitle->getPrefixedText()
 -                      );
 -              }
 -
 -              // Load the data from the master database if needed.
 -              // The caller may already loaded it from the master or even loaded it using
 -              // SELECT FOR UPDATE, so do not override that using clear().
 -              $this->loadPageData( 'fromdbmaster' );
 -
 -              $user = $user ?: $wgUser;
 -              $flags = $this->checkFlags( $flags );
 -
 -              // Avoid PHP 7.1 warning of passing $this by reference
 -              $wikiPage = $this;
 -
 -              // Trigger pre-save hook (using provided edit summary)
 -              $hookStatus = Status::newGood( [] );
 -              $hook_args = [ &$wikiPage, &$user, &$content, &$summary,
 -                                                      $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
 -              // Check if the hook rejected the attempted save
 -              if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
 -                      if ( $hookStatus->isOK() ) {
 -                              // Hook returned false but didn't call fatal(); use generic message
 -                              $hookStatus->fatal( 'edit-hook-aborted' );
 -                      }
 -
 -                      return $hookStatus;
 -              }
 -
 -              $old_revision = $this->getRevision(); // current revision
 -              $old_content = $this->getContent( Revision::RAW ); // current revision's content
 -
 -              $handler = $content->getContentHandler();
 -              $tag = $handler->getChangeTag( $old_content, $content, $flags );
 -              // If there is no applicable tag, null is returned, so we need to check
 -              if ( $tag ) {
 -                      $tags[] = $tag;
 -              }
 -
 -              // Check for undo tag
 -              if ( $undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
 -                      $tags[] = 'mw-undo';
 -              }
 -
 -              // Provide autosummaries if summary is not provided and autosummaries are enabled
 -              if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
 -                      $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, $useCache );
 -              $pstContent = $editInfo->pstContent; // Content object
 -              $meta = [
 -                      'bot' => ( $flags & EDIT_FORCE_BOT ),
 -                      'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
 -                      'serialized' => $pstContent->serialize( $serialFormat ),
 -                      'serialFormat' => $serialFormat,
 -                      'baseRevId' => $baseRevId,
 -                      'oldRevision' => $old_revision,
 -                      'oldContent' => $old_content,
 -                      'oldId' => $this->getLatest(),
 -                      'oldIsRedirect' => $this->isRedirect(),
 -                      'oldCountable' => $this->isCountable(),
 -                      'tags' => ( $tags !== null ) ? (array)$tags : [],
 -                      'undidRevId' => $undidRevId
 -              ];
 -
 -              // Actually create the revision and create/update the page
 -              if ( $flags & EDIT_UPDATE ) {
 -                      $status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
 -              } else {
 -                      $status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
 +              if ( !$user ) {
 +                      $user = $wgUser;
                }
  
 -              // Promote user to any groups they meet the criteria for
 -              DeferredUpdates::addCallableUpdate( function () use ( $user ) {
 -                      $user->addAutopromoteOnceGroups( 'onEdit' );
 -                      $user->addAutopromoteOnceGroups( 'onView' ); // b/c
 -              } );
 -
 -              return $status;
 -      }
 -
 -      /**
 -       * @param Content $content Pre-save transform content
 -       * @param int $flags
 -       * @param User $user
 -       * @param string $summary
 -       * @param array $meta
 -       * @return Status
 -       * @throws DBUnexpectedError
 -       * @throws Exception
 -       * @throws FatalError
 -       * @throws MWException
 -       */
 -      private function doModify(
 -              Content $content, $flags, User $user, $summary, array $meta
 -      ) {
 -              global $wgUseRCPatrol;
 -
 -              // Update article, but only if changed.
 -              $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
 -
 -              // Convenience variables
 -              $now = wfTimestampNow();
 -              $oldid = $meta['oldId'];
 -              /** @var Content|null $oldContent */
 -              $oldContent = $meta['oldContent'];
 -              $newsize = $content->getSize();
 -
 -              if ( !$oldid ) {
 -                      // Article gone missing
 -                      $status->fatal( 'edit-gone-missing' );
 -
 -                      return $status;
 -              } elseif ( !$oldContent ) {
 -                      // Sanity check for T39225
 -                      throw new MWException( "Could not find text for current revision {$oldid}." );
 +              // TODO: this check is here for backwards-compatibility with 1.31 behavior.
 +              // Checking the minoredit right should be done in the same place the 'bot' right is
 +              // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
 +              if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
 +                      $flags = ( $flags & ~EDIT_MINOR );
                }
  
 -              $changed = !$content->equals( $oldContent );
 -
 -              $dbw = wfGetDB( DB_MASTER );
 +              // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
 +              // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
 +              // used by this PageUpdater. However, there is no guarantee for this.
 +              $updater = $this->newPageUpdater( $user );
 +              $updater->setContent( 'main', $content );
 +              $updater->setBaseRevisionId( $baseRevId );
 +              $updater->setUndidRevisionId( $undidRevId );
  
 -              if ( $changed ) {
 -                      // @TODO: pass content object?!
 -                      $revision = new Revision( [
 -                              'page'       => $this->getId(),
 -                              'title'      => $this->mTitle, // for determining the default content model
 -                              'comment'    => $summary,
 -                              'minor_edit' => $meta['minor'],
 -                              'text'       => $meta['serialized'],
 -                              'len'        => $newsize,
 -                              'parent_id'  => $oldid,
 -                              'user'       => $user->getId(),
 -                              'user_text'  => $user->getName(),
 -                              'timestamp'  => $now,
 -                              'content_model' => $content->getModel(),
 -                              'content_format' => $meta['serialFormat'],
 -                      ] );
 +              $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
  
 -                      $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
 -                      $status->merge( $prepStatus );
 -                      if ( !$status->isOK() ) {
 -                              return $status;
 -                      }
 -
 -                      $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->endAtomic( __METHOD__ );
 -                              // Page updated or deleted in the mean time
 -                              $status->fatal( 'edit-conflict' );
 -
 -                              return $status;
 -                      }
 -
 -                      // 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).
 -
 -                      // Save the revision text
 -                      $revisionId = $revision->insertOn( $dbw );
 -                      // Update page_latest and friends to reflect the new revision
 -                      if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
 -                              throw new MWException( "Failed to update page row to use new revision." );
 -                      }
 -
 -                      $tags = $meta['tags'];
 -                      Hooks::run( 'NewRevisionFromEditComplete',
 -                              [ $this, $revision, $meta['baseRevId'], $user, &$tags ] );
 -
 -                      // Update recentchanges
 -                      if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
 -                              // Mark as patrolled if the user can do so
 -                              $autopatrolled = $wgUseRCPatrol && !count(
 -                                              $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
 -                              // Add RC row to the DB
 -                              RecentChange::notifyEdit(
 -                                      $now,
 -                                      $this->mTitle,
 -                                      $revision->isMinor(),
 -                                      $user,
 -                                      $summary,
 -                                      $oldid,
 -                                      $this->getTimestamp(),
 -                                      $meta['bot'],
 -                                      '',
 -                                      $oldContent ? $oldContent->getSize() : 0,
 -                                      $newsize,
 -                                      $revisionId,
 -                                      $autopatrolled ? RecentChange::PRC_AUTOPATROLLED :
 -                                              RecentChange::PRC_UNPATROLLED,
 -                                      $tags
 -                              );
 -                      }
 -
 -                      $user->incEditCount();
 -
 -                      $dbw->endAtomic( __METHOD__ );
 -                      $this->mTimestamp = $now;
 -              } else {
 -                      // T34948: revision ID must be set to page {{REVISIONID}} and
 -                      // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
 -                      // Since we don't insert a new revision into the database, the least
 -                      // error-prone way is to reuse given old revision.
 -                      $revision = $meta['oldRevision'];
 +              // TODO: this logic should not be in the storage layer, it's here for compatibility
 +              // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
 +              // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
 +              if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
 +                      $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
                }
  
 -              if ( $changed ) {
 -                      // Return the new revision to the caller
 -                      $status->value['revision'] = $revision;
 -              } else {
 -                      $status->warning( 'edit-no-change' );
 -                      // Update page_touched as updateRevisionOn() was not called.
 -                      // Other cache updates are managed in onArticleEdit() via doEditUpdates().
 -                      $this->mTitle->invalidateCache( $now );
 -              }
 +              $updater->addTags( $tags );
  
 -              // 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']
 -                                              ]
 -                                      );
 -                                      // Avoid PHP 7.1 warning of passing $this by reference
 -                                      $wikiPage = $this;
 -                                      // Trigger post-save hook
 -                                      $params = [ &$wikiPage, &$user, $content, $summary, $flags & EDIT_MINOR,
 -                                              null, null, &$flags, $revision, &$status, $meta['baseRevId'],
 -                                              $meta['undidRevId'] ];
 -                                      Hooks::run( 'PageContentSaveComplete', $params );
 -                              }
 -                      ),
 -                      DeferredUpdates::PRESEND
 +              $revRec = $updater->saveRevision(
 +                      $summary,
 +                      $flags
                );
  
 -              return $status;
 -      }
 -
 -      /**
 -       * @param Content $content Pre-save transform content
 -       * @param int $flags
 -       * @param User $user
 -       * @param string $summary
 -       * @param array $meta
 -       * @return Status
 -       * @throws DBUnexpectedError
 -       * @throws Exception
 -       * @throws FatalError
 -       * @throws MWException
 -       */
 -      private function doCreate(
 -              Content $content, $flags, User $user, $summary, array $meta
 -      ) {
 -              global $wgUseRCPatrol, $wgUseNPPatrol, $wgPageCreationLog;
 -
 -              $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
 -
 -              $now = wfTimestampNow();
 -              $newsize = $content->getSize();
 -              $prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
 -              $status->merge( $prepStatus );
 -              if ( !$status->isOK() ) {
 -                      return $status;
 -              }
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -              $dbw->startAtomic( __METHOD__ );
 -
 -              // Add the page record unless one already exists for the title
 -              $newid = $this->insertOn( $dbw );
 -              if ( $newid === false ) {
 -                      $dbw->endAtomic( __METHOD__ ); // nothing inserted
 -                      $status->fatal( 'edit-already-exists' );
 -
 -                      return $status; // nothing done
 -              }
 -
 -              // 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).
 -
 -              // @TODO: pass content object?!
 -              $revision = new Revision( [
 -                      'page'       => $newid,
 -                      'title'      => $this->mTitle, // for determining the default content model
 -                      'comment'    => $summary,
 -                      'minor_edit' => $meta['minor'],
 -                      'text'       => $meta['serialized'],
 -                      'len'        => $newsize,
 -                      'user'       => $user->getId(),
 -                      'user_text'  => $user->getName(),
 -                      '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 ) ) {
 -                      throw new MWException( "Failed to update page row to use new revision." );
 -              }
 -
 -              Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
 -
 -              // Update recentchanges
 -              if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
 -                      // Mark as patrolled if the user can do so
 -                      $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
 -                              !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
 -                      // Add RC row to the DB
 -                      RecentChange::notifyNew(
 -                              $now,
 -                              $this->mTitle,
 -                              $revision->isMinor(),
 -                              $user,
 -                              $summary,
 -                              $meta['bot'],
 -                              '',
 -                              $newsize,
 -                              $revisionId,
 -                              $patrolled,
 -                              $meta['tags']
 -                      );
 -              }
 -
 -              $user->incEditCount();
 -
 -              if ( $wgPageCreationLog ) {
 -                      // Log the page creation
 -                      // @TODO: Do we want a 'recreate' action?
 -                      $logEntry = new ManualLogEntry( 'create', 'create' );
 -                      $logEntry->setPerformer( $user );
 -                      $logEntry->setTarget( $this->mTitle );
 -                      $logEntry->setComment( $summary );
 -                      $logEntry->setTimestamp( $now );
 -                      $logEntry->setAssociatedRevId( $revisionId );
 -                      $logid = $logEntry->insert();
 -                      // Note that we don't publish page creation events to recentchanges
 -                      // (i.e. $logEntry->publish()) since this would create duplicate entries,
 -                      // one for the edit and one for the page creation.
 +              // $revRec will be null if the edit failed, or if no new revision was created because
 +              // the content did not change.
 +              if ( $revRec ) {
 +                      // update cached fields
 +                      // TODO: this is currently redundant to what is done in updateRevisionOn.
 +                      // But updateRevisionOn() should move into PageStore, and then this will be needed.
 +                      $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
 +                      $this->mLatest = $revRec->getId();
                }
  
 -              $dbw->endAtomic( __METHOD__ );
 -              $this->mTimestamp = $now;
 -
 -              // 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 ] );
 -                                      // Avoid PHP 7.1 warning of passing $this by reference
 -                                      $wikiPage = $this;
 -                                      // Trigger post-create hook
 -                                      $params = [ &$wikiPage, &$user, $content, $summary,
 -                                                              $flags & EDIT_MINOR, null, null, &$flags, $revision ];
 -                                      Hooks::run( 'PageContentInsertComplete', $params );
 -                                      // Trigger post-save hook
 -                                      $params = array_merge( $params, [ &$status, $meta['baseRevId'], 0 ] );
 -                                      Hooks::run( 'PageContentSaveComplete', $params );
 -                              }
 -                      ),
 -                      DeferredUpdates::PRESEND
 -              );
 -
 -              return $status;
 +              return $updater->getStatus();
        }
  
        /**
        /**
         * Prepare content which is about to be saved.
         *
 -       * Prior to 1.30, this returned a stdClass object with the same class
 -       * members.
 +       * Prior to 1.30, this returned a stdClass.
 +       *
 +       * @deprecated since 1.32, use getDerivedDataUpdater instead.
         *
         * @param Content $content
 -       * @param Revision|int|null $revision Revision object. For backwards compatibility, a
 -       *        revision ID is also accepted, but this is deprecated.
 +       * @param Revision|RevisionRecord|int|null $revision Revision object.
 +       *        For backwards compatibility, a revision ID is also accepted,
 +       *        but this is deprecated.
 +       *        Used with vary-revision or vary-revision-id.
         * @param User|null $user
 -       * @param string|null $serialFormat
 +       * @param string|null $serialFormat IGNORED
         * @param bool $useCache Check shared prepared edit cache
         *
         * @return PreparedEdit
         * @since 1.21
         */
        public function prepareContentForEdit(
 -              Content $content, $revision = null, User $user = null,
 -              $serialFormat = null, $useCache = true
 +              Content $content,
 +              $revision = null,
 +              User $user = null,
 +              $serialFormat = null,
 +              $useCache = true
        ) {
 -              global $wgContLang, $wgUser, $wgAjaxEditStash;
 +              global $wgUser;
  
 -              if ( is_object( $revision ) ) {
 -                      $revid = $revision->getId();
 -              } else {
 +              if ( !$user ) {
 +                      $user = $wgUser;
 +              }
 +
 +              if ( !is_object( $revision ) ) {
                        $revid = $revision;
                        // This code path is deprecated, and nothing is known to
                        // use it, so performance here shouldn't be a worry.
                        if ( $revid !== null ) {
                                wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
 -                              $revision = Revision::newFromId( $revid, Revision::READ_LATEST );
 +                              $store = $this->getRevisionStore();
 +                              $revision = $store->getRevisionById( $revid, Revision::READ_LATEST );
                        } else {
                                $revision = null;
                        }
 +              } elseif ( $revision instanceof Revision ) {
 +                      $revision = $revision->getRevisionRecord();
                }
  
 -              $user = is_null( $user ) ? $wgUser : $user;
 -              // XXX: check $user->getId() here???
 +              $slots = RevisionSlotsUpdate::newFromContent( [ 'main' => $content ] );
 +              $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
  
 -              // Use a sane default for $serialFormat, see T59026
 -              if ( $serialFormat === null ) {
 -                      $serialFormat = $content->getContentHandler()->getDefaultFormat();
 -              }
 +              if ( !$updater->isUpdatePrepared() ) {
 +                      $updater->prepareContent( $user, $slots, [], $useCache );
  
 -              if ( $this->mPreparedEdit
 -                      && isset( $this->mPreparedEdit->newContent )
 -                      && $this->mPreparedEdit->newContent->equals( $content )
 -                      && $this->mPreparedEdit->revid == $revid
 -                      && $this->mPreparedEdit->format == $serialFormat
 -                      // XXX: also check $user here?
 -              ) {
 -                      // Already prepared
 -                      return $this->mPreparedEdit;
 -              }
 -
 -              // The edit may have already been prepared via api.php?action=stashedit
 -              $cachedEdit = $useCache && $wgAjaxEditStash
 -                      ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
 -                      : false;
 -
 -              $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
 -              Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
 -
 -              $edit = new PreparedEdit();
 -              if ( $cachedEdit ) {
 -                      $edit->timestamp = $cachedEdit->timestamp;
 -              } else {
 -                      $edit->timestamp = wfTimestampNow();
 -              }
 -              // @note: $cachedEdit is safely not used if the rev ID was referenced in the text
 -              $edit->revid = $revid;
 -
 -              if ( $cachedEdit ) {
 -                      $edit->pstContent = $cachedEdit->pstContent;
 -              } else {
 -                      $edit->pstContent = $content
 -                              ? $content->preSaveTransform( $this->mTitle, $user, $popts )
 -                              : null;
 -              }
 -
 -              $edit->format = $serialFormat;
 -              $edit->popts = $this->makeParserOptions( 'canonical' );
 -              if ( $cachedEdit ) {
 -                      $edit->output = $cachedEdit->output;
 -              } else {
                        if ( $revision ) {
 -                              // 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 replica DB lag.
 -                              $oldCallback = $edit->popts->getCurrentRevisionCallback();
 -                              $edit->popts->setCurrentRevisionCallback(
 -                                      function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
 -                                              if ( $title->equals( $revision->getTitle() ) ) {
 -                                                      return $revision;
 -                                              } else {
 -                                                      return call_user_func( $oldCallback, $title, $parser );
 -                                              }
 -                                      }
 -                              );
 -                      } else {
 -                              // Try to avoid a second parse if {{REVISIONID}} is used
 -                              $dbIndex = ( $this->mDataLoadedFrom & self::READ_LATEST ) === self::READ_LATEST
 -                                      ? DB_MASTER // use the best possible guess
 -                                      : DB_REPLICA; // T154554
 -
 -                              $edit->popts->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
 -                                      $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
 -                                      // Use a fresh connection in order to see the latest data, by avoiding
 -                                      // stale data from REPEATABLE-READ snapshots.
 -                                      $db = $lb->getConnectionRef( $dbIndex, [], false, $lb::CONN_TRX_AUTO );
 -
 -                                      return 1 + (int)$db->selectField(
 -                                              'revision',
 -                                              'MAX(rev_id)',
 -                                              [],
 -                                              __METHOD__
 -                                      );
 -                              } );
 +                              $updater->prepareUpdate( $revision );
                        }
 -                      $edit->output = $edit->pstContent
 -                              ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
 -                              : null;
                }
  
 -              $edit->newContent = $content;
 -              $edit->oldContent = $this->getContent( Revision::RAW );
 -
 -              if ( $edit->output ) {
 -                      $edit->output->setCacheTime( wfTimestampNow() );
 -              }
 -
 -              // Process cache the result
 -              $this->mPreparedEdit = $edit;
 -
 -              return $edit;
 +              return $updater->getPreparedEdit();
        }
  
        /**
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
 +       * @deprecated since 1.32, use PageUpdater::doEditUpdates instead.
 +       *
         * @param Revision $revision
         * @param User $user User object that did the revision
         * @param array $options Array of options, following indexes are used:
         *   - 'no-change': don't update the article count, ever
         */
        public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
 -              global $wgRCWatchCategoryMembership;
 -
 -              $options += [
 -                      'changed' => true,
 -                      'created' => false,
 -                      'moved' => false,
 -                      'restored' => false,
 -                      'oldrevision' => null,
 -                      'oldcountable' => null
 -              ];
 -              $content = $revision->getContent();
 -
 -              $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__ . ": 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.
 -              MediaWikiServices::getInstance()->getParserCache()->save(
 -                      $editInfo->output, $this, $editInfo->popts,
 -                      $revision->getTimestamp(), $editInfo->revid
 -              );
 -
 -              // Update the links tables and other secondary data
 -              if ( $content ) {
 -                      $recursive = $options['changed']; // T52785
 -                      $updates = $content->getSecondaryDataUpdates(
 -                              $this->getTitle(), null, $recursive, $editInfo->output
 -                      );
 -                      foreach ( $updates as $update ) {
 -                              $update->setCause( 'edit-page', $user->getName() );
 -                              if ( $update instanceof LinksUpdate ) {
 -                                      $update->setRevision( $revision );
 -                                      $update->setTriggeringUser( $user );
 -                              }
 -                              DeferredUpdates::addUpdate( $update );
 -                      }
 -                      if ( $wgRCWatchCategoryMembership
 -                              && $this->getContentHandler()->supportsCategories() === true
 -                              && ( $options['changed'] || $options['created'] )
 -                              && !$options['restored']
 -                      ) {
 -                              // Note: jobs are pushed after deferred updates, so the job should be able to see
 -                              // the recent change entry (also done via deferred updates) and carry over any
 -                              // bot/deletion/IP flags, ect.
 -                              JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
 -                                      $this->getTitle(),
 -                                      [
 -                                              'pageId' => $this->getId(),
 -                                              'revTimestamp' => $revision->getTimestamp()
 -                                      ]
 -                              ) );
 -                      }
 -              }
 -
 -              // Avoid PHP 7.1 warning of passing $this by reference
 -              $wikiPage = $this;
 -
 -              Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $options['changed'] ] );
 -
 -              if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
 -                      // Flush old entries from the `recentchanges` table
 -                      if ( mt_rand( 0, 9 ) == 0 ) {
 -                              JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
 -                      }
 -              }
 -
 -              if ( !$this->exists() ) {
 -                      return;
 -              }
 -
 -              $id = $this->getId();
 -              $title = $this->mTitle->getPrefixedDBkey();
 -              $shortTitle = $this->mTitle->getDBkey();
 -
 -              if ( $options['oldcountable'] === 'no-change' ||
 -                      ( !$options['changed'] && !$options['moved'] )
 -              ) {
 -                      $good = 0;
 -              } elseif ( $options['created'] ) {
 -                      $good = (int)$this->isCountable( $editInfo );
 -              } elseif ( $options['oldcountable'] !== null ) {
 -                      $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
 -              } else {
 -                      $good = 0;
 -              }
 -              $edits = $options['changed'] ? 1 : 0;
 -              $pages = $options['created'] ? 1 : 0;
 +              $revision = $revision->getRevisionRecord();
  
 -              DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
 -                      [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
 -              ) );
 -              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
 -
 -              // If this is another user's talk page, update newtalk.
 -              // Don't do this if $options['changed'] = false (null-edits) nor if
 -              // it's a minor edit and the user doesn't want notifications for those.
 -              if ( $options['changed']
 -                      && $this->mTitle->getNamespace() == NS_USER_TALK
 -                      && $shortTitle != $user->getTitleKey()
 -                      && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
 -              ) {
 -                      $recipient = User::newFromName( $shortTitle, false );
 -                      if ( !$recipient ) {
 -                              wfDebug( __METHOD__ . ": invalid username\n" );
 -                      } else {
 -                              // Avoid PHP 7.1 warning of passing $this by reference
 -                              $wikiPage = $this;
 -
 -                              // Allow extensions to prevent user notification
 -                              // when a new message is added to their talk page
 -                              if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
 -                                      if ( User::isIP( $shortTitle ) ) {
 -                                              // An anonymous user
 -                                              $recipient->setNewtalk( true, $revision );
 -                                      } elseif ( $recipient->isLoggedIn() ) {
 -                                              $recipient->setNewtalk( true, $revision );
 -                                      } else {
 -                                              wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
 -                                      }
 -                              }
 -                      }
 -              }
 +              $updater = $this->getDerivedDataUpdater( $user, $revision );
  
 -              if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 -                      MessageCache::singleton()->updateMessageOverride( $this->mTitle, $content );
 -              }
 +              $updater->prepareUpdate( $revision, $options );
  
 -              if ( $options['created'] ) {
 -                      self::onArticleCreate( $this->mTitle );
 -              } elseif ( $options['changed'] ) { // T52785
 -                      self::onArticleEdit( $this->mTitle, $revision );
 -              }
 -
 -              ResourceLoaderWikiModule::invalidateModuleCache(
 -                      $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
 -              );
 +              $updater->doUpdates();
        }
  
        /**
                // Take this opportunity to purge out expired restrictions
                Title::purgeExpiredRestrictions();
  
 -              // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
 +              // @todo: Same limitations as described in ProtectionForm.php (line 37);
                // we expect a single selection, but the schema allows otherwise.
                $isProtected = false;
                $protect = false;
                $tags = [], $logsubtype = 'delete'
        ) {
                global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
 -                      $wgActorTableSchemaMigrationStage;
 +                      $wgActorTableSchemaMigrationStage, $wgMultiContentRevisionSchemaMigrationStage;
  
                wfDebug( __METHOD__ . "\n" );
  
                // Note array_intersect() preserves keys from the first arg, and we're
                // assuming $revQuery has `revision` primary and isn't using subtables
                // for anything we care about.
 +              $tablesFlat = [];
 +              array_walk_recursive(
 +                      $revQuery['tables'],
 +                      function ( $a ) use ( &$tablesFlat ) {
 +                              $tablesFlat[] = $a;
 +                      }
 +              );
 +
                $res = $dbw->select(
                        array_intersect(
 -                              $revQuery['tables'],
 +                              $tablesFlat,
                                [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
                        ),
                        '1',
                                '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_id should probably not be written to when the multi content schema has
 +                                       * been migrated to (wgMultiContentRevisionSchemaMigrationStage) however there is no
 +                                       * default for the field in WMF production currently so we must keep writing
 +                                       * writing until a default of 0 is set.
 +                                       * Task: https://phabricator.wikimedia.org/T190148
 +                                       * Copying the value from the revision table should not lead to any issues for now.
 +                                       */
                                'ar_len'        => $row->rev_len,
                                'ar_page_id'    => $id,
                                'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
                                'ar_sha1'       => $row->rev_sha1,
                        ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
                                + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
 -                      if ( $wgContentHandlerUseDB ) {
 +
 +                      if ( $wgMultiContentRevisionSchemaMigrationStage < MIGRATION_NEW ) {
 +                              $rowInsert['ar_text_id'] = $row->rev_text_id;
 +                      }
 +
 +                      if (
 +                              $wgContentHandlerUseDB &&
 +                              $wgMultiContentRevisionSchemaMigrationStage <= MIGRATION_WRITE_BOTH
 +                      ) {
                                $rowInsert['ar_content_model'] = $row->rev_content_model;
                                $rowInsert['ar_content_format'] = $row->rev_content_format;
                        }
         * @param Title $title
         */
        public static function onArticleCreate( Title $title ) {
 +              // TODO: move this into a PageEventEmitter service
 +
                // Update existence markers on article/talk tabs...
                $other = $title->getOtherPage();
  
         * @param Title $title
         */
        public static function onArticleDelete( Title $title ) {
 +              // TODO: move this into a PageEventEmitter service
 +
                // Update existence markers on article/talk tabs...
                // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
                BacklinkCache::get( $title )->clear();
         *
         * @param Title $title
         * @param Revision|null $revision Revision that was just saved, may be null
 +       * @param string[]|null $slotsChanged The role names of the slots that were changed.
 +       *        If not given, all slots are assumed to have changed.
         */
 -      public static function onArticleEdit( Title $title, Revision $revision = null ) {
 -              // Invalidate caches of articles which include this page
 -              DeferredUpdates::addUpdate(
 -                      new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
 -              );
 +      public static function onArticleEdit(
 +              Title $title,
 +              Revision $revision = null,
 +              $slotsChanged = null
 +      ) {
 +              // TODO: move this into a PageEventEmitter service
 +
 +              if ( $slotsChanged === null || in_array( 'main',  $slotsChanged ) ) {
 +                      // Invalidate caches of articles which include this page.
 +                      // Only for the main slot, because only the main slot is transcluded.
 +                      // TODO: MCR: not true for TemplateStyles! [SlotHandler]
 +                      DeferredUpdates::addUpdate(
 +                              new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
 +                      );
 +              }
  
                // Invalidate the caches of all pages which redirect here
                DeferredUpdates::addUpdate(
  
                return $linkCache->getMutableCacheKeys( $cache, $this->getTitle()->getTitleValue() );
        }
 +
  }
@@@ -449,7 -449,7 +449,7 @@@ class CoreParserFunctions 
                                $parser->mOutput->setDisplayTitle( $text );
                        }
                        if ( $old !== false && $old !== $text && !$arg ) {
 -                              $converter = $parser->getConverterLanguage()->getConverter();
 +                              $converter = $parser->getTargetLanguage()->getConverter();
                                return '<span class="error">' .
                                        wfMessage( 'duplicate-displaytitle',
                                                // Message should be parsed, but these params should only be escaped.
                                return '';
                        }
                } else {
 -                      $converter = $parser->getConverterLanguage()->getConverter();
 +                      $converter = $parser->getTargetLanguage()->getConverter();
                        $parser->getOutput()->addWarning(
                                wfMessage( 'restricted-displaytitle',
                                        // Message should be parsed, but this param should only be escaped.
                if ( $old === false || $old == $text || $arg ) {
                        return '';
                } else {
 -                      $converter = $parser->getConverterLanguage()->getConverter();
 +                      $converter = $parser->getTargetLanguage()->getConverter();
                        return '<span class="error">' .
                                wfMessage( 'duplicate-defaultsort',
                                        // Message should be parsed, but these params should only be escaped.
                }
  
                // Check the link cache, maybe something already looked it up.
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $pdbk = $t->getPrefixedDBkey();
                $id = $linkCache->getGoodLinkID( $pdbk );
                if ( $id != 0 ) {
@@@ -460,11 -460,11 +460,11 @@@ class Parser 
                        || isset( $this->mDoubleUnderscores['notitleconvert'] )
                        || $this->mOutput->getDisplayTitle() !== false )
                ) {
 -                      $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
 +                      $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
                        if ( $convruletitle ) {
                                $this->mOutput->setTitleText( $convruletitle );
                        } else {
 -                              $titleText = $this->getConverterLanguage()->convertTitle( $title );
 +                              $titleText = $this->getTargetLanguage()->convertTitle( $title );
                                $this->mOutput->setTitleText( $titleText );
                        }
                }
  
        /**
         * Get the language object for language conversion
 +       * @deprecated since 1.32, just use getTargetLanguage()
         * @return Language|null
         */
        public function getConverterLanguage() {
                                # The position of the convert() call should not be changed. it
                                # assumes that the links are all replaced and the only thing left
                                # is the <nowiki> mark.
 -                              $text = $this->getConverterLanguage()->convert( $text );
 +                              $text = $this->getTargetLanguage()->convert( $text );
                        }
                }
  
                if ( $text === false ) {
                        # Not an image, make a link
                        $text = Linker::makeExternalLink( $url,
 -                              $this->getConverterLanguage()->markNoConversion( $url, true ),
 +                              $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
                                true, 'free',
                                $this->getExternalLinkAttribs( $url ), $this->mTitle );
                        # Register it in the output object...
                                list( $dtrail, $trail ) = Linker::splitTrail( $trail );
                        }
  
 -                      $text = $this->getConverterLanguage()->markNoConversion( $text );
 +                      // Excluding protocol-relative URLs may avoid many false positives.
 +                      if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
 +                              $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
 +                      }
  
                        $url = Sanitizer::cleanUrl( $url );
  
                                        }
                                        $sortkey = Sanitizer::decodeCharReferences( $sortkey );
                                        $sortkey = str_replace( "\n", '', $sortkey );
 -                                      $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
 +                                      $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
                                        $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
  
                                        continue;
                                        $this->mOutput->setFlag( 'vary-revision' );
                                        wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
                                }
 -                              $value = $pageid ? $pageid : null;
 +                              $value = $pageid ?: null;
                                break;
                        case 'revisionid':
                                # Let the edit saving system know we should parse the page
                        if ( $title ) {
                                $titleText = $title->getPrefixedText();
                                # Check for language variants if the template is not found
 -                              if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
 -                                      $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
 +                              if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
 +                                      $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
                                }
                                # Do recursion depth check
                                $limit = $this->mOptions->getMaxTemplateDepth();
                        $rev_id = $rev ? $rev->getId() : 0;
                        # If there is no current revision, there is no page
                        if ( $id === false && !$rev ) {
-                               $linkCache = LinkCache::singleton();
+                               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                                $linkCache->addBadLinkObj( $title );
                        }
  
@@@ -178,55 -178,6 +178,55 @@@ abstract class MediaWikiTestCase extend
                return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
        }
  
 +      /**
 +       * Returns a WikiPage representing an existing page.
 +       *
 +       * @since 1.32
 +       *
 +       * @param Title|string|null $title
 +       * @return WikiPage
 +       * @throws MWException
 +       */
 +      protected function getExistingTestPage( $title = null ) {
 +              $title = ( $title === null ) ? 'UTPage' : $title;
 +              $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
 +              $page = WikiPage::factory( $title );
 +
 +              if ( !$page->exists() ) {
 +                      $user = self::getTestSysop()->getUser();
 +                      $page->doEditContent(
 +                              new WikitextContent( 'UTContent' ),
 +                              'UTPageSummary',
 +                              EDIT_NEW | EDIT_SUPPRESS_RC,
 +                              false,
 +                              $user
 +                      );
 +              }
 +
 +              return $page;
 +      }
 +
 +      /**
 +       * Returns a WikiPage representing a non-existing page.
 +       *
 +       * @since 1.32
 +       *
 +       * @param Title|string|null $title
 +       * @return WikiPage
 +       * @throws MWException
 +       */
 +      protected function getNonexistingTestPage( $title = null ) {
 +              $title = ( $title === null ) ? 'UTPage-' . rand( 0, 100000 ) : $title;
 +              $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
 +              $page = WikiPage::factory( $title );
 +
 +              if ( $page->exists() ) {
 +                      $page->doDeleteArticle( 'Testing' );
 +              }
 +
 +              return $page;
 +      }
 +
        /**
         * Prepare service configuration for unit testing.
         *
         * Should be called from addDBData().
         *
         * @since 1.25 ($namespace in 1.28)
 -       * @param string|title $pageName Page name or title
 +       * @param string|Title $pageName Page name or title
         * @param string $text Page's content
         * @param int $namespace Namespace id (name cannot already contain namespace)
         * @return array Title object and page id
        public function addDBData() {
        }
  
 -      private function addCoreDBData() {
 +      /**
 +       * @since 1.32
 +       */
 +      protected function addCoreDBData() {
                if ( $this->db->getType() == 'oracle' ) {
                        # Insert 0 user to prevent FK violations
                        # Anonymous user
        /**
         * @throws LogicException if the given database connection is not a set up to use
         * mock tables.
 +       *
 +       * @since 1.31 this is no longer private.
         */
 -      private function ensureMockDatabaseConnection( IDatabase $db ) {
 +      protected function ensureMockDatabaseConnection( IDatabase $db ) {
                if ( $db->tablePrefix() !== $this->dbPrefix() ) {
                        throw new LogicException(
                                'Trying to delete mock tables, but table prefix does not indicate a mock database.'
                        if ( $tbl === 'page' ) {
                                // Forget about the pages since they don't
                                // exist in the DB.
-                               LinkCache::singleton()->clear();
+                               MediaWikiServices::getInstance()->getLinkCache()->clear();
                        }
                }
        }
        private function resetDB( $db, $tablesUsed ) {
                if ( $db ) {
                        $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
 -                      $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp',
 -                              'revision_actor_temp', 'comment', 'archive' ];
 +                      $pageTables = [
 +                              'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
 +                              'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
 +                      ];
                        $coreDBDataTables = array_merge( $userTables, $pageTables );
  
                        // If any of the user or page tables were marked as used, we should clear all of them.
                                if ( $tbl === 'page' ) {
                                        // Forget about the pages since they don't
                                        // exist in the DB.
-                                       LinkCache::singleton()->clear();
+                                       MediaWikiServices::getInstance()->getLinkCache()->clear();
                                }
                        }