Merge "Title::getTalkPage(): Restore behavior of interwiki-prefixed & fragment-only...
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 16 Sep 2019 23:16:16 +0000 (23:16 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 16 Sep 2019 23:16:16 +0000 (23:16 +0000)
1  2 
includes/Title.php
tests/phpunit/includes/TitleTest.php

diff --combined includes/Title.php
@@@ -23,7 -23,7 +23,7 @@@
   */
  
  use MediaWiki\Permissions\PermissionManager;
 -use MediaWiki\Storage\RevisionRecord;
 +use MediaWiki\Revision\RevisionRecord;
  use Wikimedia\Assert\Assert;
  use Wikimedia\Rdbms\Database;
  use Wikimedia\Rdbms\IDatabase;
@@@ -51,11 -51,10 +51,11 @@@ class Title implements LinkTarget, IDBA
        const CACHE_MAX = 1000;
  
        /**
 -       * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
 -       * to use the master DB
 +       * Used to be GAID_FOR_UPDATE define(). Used with getArticleID() and friends
 +       * to use the master DB and inject it into link cache.
 +       * @deprecated since 1.34, use Title::READ_LATEST instead.
         */
 -      const GAID_FOR_UPDATE = 1;
 +      const GAID_FOR_UPDATE = 512;
  
        /**
         * Flag for use with factory methods like newFromLinkTarget() that have
  
        /** @var string Text form (spaces not underscores) of the main part */
        public $mTextform = '';
 -
        /** @var string URL-encoded form of the main part */
        public $mUrlform = '';
 -
        /** @var string Main part with underscores */
        public $mDbkeyform = '';
 -
        /** @var string Database key with the initial letter in the case specified by the user */
        protected $mUserCaseDBKey;
 -
        /** @var int Namespace index, i.e. one of the NS_xxxx constants */
        public $mNamespace = NS_MAIN;
 -
        /** @var string Interwiki prefix */
        public $mInterwiki = '';
 -
        /** @var bool Was this Title created from a string with a local interwiki prefix? */
        private $mLocalInterwiki = false;
 -
        /** @var string Title fragment (i.e. the bit after the #) */
        public $mFragment = '';
  
        /** @var bool Whether a page has any subpages */
        private $mHasSubpages;
  
 -      /** @var bool The (string) language code of the page's language and content code. */
 -      private $mPageLanguage = false;
 +      /** @var array|null The (string) language code of the page's language and content code. */
 +      private $mPageLanguage;
  
        /** @var string|bool|null The page language code from the database, null if not saved in
         * the database or false if not loaded, yet.
         * Create a new Title from an article ID
         *
         * @param int $id The page_id corresponding to the Title to create
 -       * @param int $flags Use Title::GAID_FOR_UPDATE to use master
 +       * @param int $flags Bitfield of class READ_* constants
         * @return Title|null The new object, or null on an error
         */
        public static function newFromID( $id, $flags = 0 ) {
 -              $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
 -              $row = $db->selectRow(
 +              $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
 +              list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
 +              $row = wfGetDB( $index )->selectRow(
                        'page',
                        self::getSelectFields(),
                        [ 'page_id' => $id ],
 -                      __METHOD__
 +                      __METHOD__,
 +                      $options
                );
                if ( $row !== false ) {
                        $title = self::newFromRow( $row );
                        if ( isset( $row->page_latest ) ) {
                                $this->mLatestID = (int)$row->page_latest;
                        }
 -                      if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
 -                              $this->mContentModel = (string)$row->page_content_model;
 -                      } elseif ( !$this->mForcedContentModel ) {
 -                              $this->mContentModel = false; # initialized lazily in getContentModel()
 +                      if ( isset( $row->page_content_model ) ) {
 +                              $this->lazyFillContentModel( $row->page_content_model );
 +                      } else {
 +                              $this->lazyFillContentModel( false ); // lazily-load getContentModel()
                        }
                        if ( isset( $row->page_lang ) ) {
                                $this->mDbPageLanguage = (string)$row->page_lang;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 -                      if ( !$this->mForcedContentModel ) {
 -                              $this->mContentModel = false; # initialized lazily in getContentModel()
 -                      }
 +                      $this->lazyFillContentModel( false ); // lazily-load getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = strtr( $title, '_', ' ' );
 -              $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
  
         * Get the prefixed DB key associated with an ID
         *
         * @param int $id The page_id of the article
 -       * @return Title|null An object representing the article, or null if no such article was found
 +       * @return string|null An object representing the article, or null if no such article was found
         */
        public static function nameOf( $id ) {
                $dbr = wfGetDB( DB_REPLICA );
                        return null;
                }
  
 -              $n = self::makeName( $s->page_namespace, $s->page_title );
 -              return $n;
 +              return self::makeName( $s->page_namespace, $s->page_title );
        }
  
        /**
         *
         * @todo Deprecate this in favor of SlotRecord::getModel()
         *
 -       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
 +       * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return string Content model id
         */
        public function getContentModel( $flags = 0 ) {
 -              if ( !$this->mForcedContentModel
 -                      && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
 -                      && $this->getArticleID( $flags )
 +              if ( $this->mForcedContentModel ) {
 +                      if ( !$this->mContentModel ) {
 +                              throw new RuntimeException( 'Got out of sync; an empty model is being forced' );
 +                      }
 +                      // Content model is locked to the currently loaded one
 +                      return $this->mContentModel;
 +              }
 +
 +              if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
 +                      $this->lazyFillContentModel( $this->loadFieldFromDB( 'page_content_model', $flags ) );
 +              } elseif (
 +                      ( !$this->mContentModel || $flags & self::GAID_FOR_UPDATE ) &&
 +                      $this->getArticleID( $flags )
                ) {
                        $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        $linkCache->addLinkObj( $this ); # in case we already had an article ID
 -                      $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
 +                      $this->lazyFillContentModel( $linkCache->getGoodLinkFieldObj( $this, 'model' ) );
                }
  
                if ( !$this->mContentModel ) {
 -                      $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 +                      $this->lazyFillContentModel( ContentHandler::getDefaultModelFor( $this ) );
                }
  
                return $this->mContentModel;
        }
  
        /**
 -       * Set a proposed content model for the page for permissions
 -       * checking. This does not actually change the content model
 -       * of a title!
 +       * Set a proposed content model for the page for permissions checking
 +       *
 +       * This does not actually change the content model of a title in the DB.
 +       * It only affects this particular Title instance. The content model is
 +       * forced to remain this value until another setContentModel() call.
         *
 -       * Additionally, you should make sure you've checked
 -       * ContentHandler::canBeUsedOn() first.
 +       * ContentHandler::canBeUsedOn() should be checked before calling this
 +       * if there is any doubt regarding the applicability of the content model
         *
         * @since 1.28
         * @param string $model CONTENT_MODEL_XXX constant
         */
        public function setContentModel( $model ) {
 +              if ( (string)$model === '' ) {
 +                      throw new InvalidArgumentException( "Missing CONTENT_MODEL_* constant" );
 +              }
 +
                $this->mContentModel = $model;
                $this->mForcedContentModel = true;
        }
  
 +      /**
 +       * If the content model field is not frozen then update it with a retreived value
 +       *
 +       * @param string|bool $model CONTENT_MODEL_XXX constant or false
 +       */
 +      private function lazyFillContentModel( $model ) {
 +              if ( !$this->mForcedContentModel ) {
 +                      $this->mContentModel = ( $model === false ) ? false : (string)$model;
 +              }
 +      }
 +
        /**
         * Get the namespace text
         *
         * @param int|int[] $namespaces,... The namespaces to check for
         * @return bool
         * @since 1.19
 +       * @suppress PhanCommentParamOnEmptyParamList Cannot make variadic due to HHVM bug, T191668#5263929
         */
        public function inNamespaces( /* ... */ ) {
                $namespaces = func_get_args();
         * Get a Title object associated with the talk page of this article
         *
         * @deprecated since 1.34, use getTalkPageIfDefined() or NamespaceInfo::getTalkPage()
-        *             with NamespaceInfo::canHaveTalkPage().
+        *             with NamespaceInfo::canHaveTalkPage(). Note that the new method will
+        *             throw if asked for the talk page of a section-only link, or of an interwiki
+        *             link.
         * @return Title The object for the talk page
         * @throws MWException if $target doesn't have talk pages, e.g. because it's in NS_SPECIAL
         *         or because it's a relative link, or an interwiki link.
         */
        public function getTalkPage() {
-               return self::castFromLinkTarget(
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) );
+               // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles.
+               //       Instead of failing on invalid titles, let's just log the issue for now.
+               //       See the discussion on T227817.
+               // Is this the same title?
+               $talkNS = MediaWikiServices::getInstance()->getNamespaceInfo()->getTalk( $this->mNamespace );
+               if ( $this->mNamespace == $talkNS ) {
+                       return $this;
+               }
+               $title = self::makeTitle( $talkNS, $this->mDbkeyform );
+               $this->warnIfPageCannotExist( $title, __METHOD__ );
+               return $title;
+               // TODO: replace the above with the code below:
+               // return self::castFromLinkTarget(
+               // MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) );
        }
  
        /**
         * @return Title The object for the subject page
         */
        public function getSubjectPage() {
-               return self::castFromLinkTarget(
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) );
+               // Is this the same title?
+               $subjectNS = MediaWikiServices::getInstance()->getNamespaceInfo()
+                       ->getSubject( $this->mNamespace );
+               if ( $this->mNamespace == $subjectNS ) {
+                       return $this;
+               }
+               // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles.
+               //       Instead of failing on invalid titles, let's just log the issue for now.
+               //       See the discussion on T227817.
+               $title = self::makeTitle( $subjectNS, $this->mDbkeyform );
+               $this->warnIfPageCannotExist( $title, __METHOD__ );
+               return $title;
+               // TODO: replace the above with the code below:
+               // return self::castFromLinkTarget(
+               // MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) );
+       }
+       /**
+        * @param Title $title
+        * @param string $method
+        *
+        * @return bool whether a warning was issued
+        */
+       private function warnIfPageCannotExist( Title $title, $method ) {
+               if ( $this->getText() == '' ) {
+                       wfLogWarning(
+                               $method . ': called on empty title ' . $this->getFullText() . ', returning '
+                               . $title->getFullText()
+                       );
+                       return true;
+               }
+               if ( $this->getInterwiki() !== '' ) {
+                       wfLogWarning(
+                               $method . ': called on interwiki title ' . $this->getFullText() . ', returning '
+                               . $title->getFullText()
+                       );
+                       return true;
+               }
+               return false;
        }
  
        /**
         * @return Title
         */
        public function getOtherPage() {
-               return self::castFromLinkTarget(
-                       MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) );
+               // NOTE: Depend on the methods in this class instead of their equivalent in NamespaceInfo,
+               //       until their semantics has become exactly the same.
+               //       See the discussion on T227817.
+               if ( $this->isSpecialPage() ) {
+                       throw new MWException( 'Special pages cannot have other pages' );
+               }
+               if ( $this->isTalkPage() ) {
+                       return $this->getSubjectPage();
+               } else {
+                       if ( !$this->canHaveTalkPage() ) {
+                               throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
+                       }
+                       return $this->getTalkPage();
+               }
+               // TODO: replace the above with the code below:
+               // return self::castFromLinkTarget(
+               // MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) );
        }
  
        /**
                                $url = false;
                                $matches = [];
  
 -                              if ( !empty( $wgActionPaths )
 +                              $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath );
 +
 +                              if ( $articlePaths
                                        && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
                                ) {
                                        $action = urldecode( $matches[2] );
 -                                      if ( isset( $wgActionPaths[$action] ) ) {
 +                                      if ( isset( $articlePaths[$action] ) ) {
                                                $query = $matches[1];
                                                if ( isset( $matches[4] ) ) {
                                                        $query .= $matches[4];
                                                }
 -                                              $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
 +                                              $url = str_replace( '$1', $dbkey, $articlePaths[$action] );
                                                if ( $query != '' ) {
                                                        $url = wfAppendQuery( $url, $query );
                                                }
                        return;
                }
  
 -              // TODO: should probably pass $flags into getArticleID, but it seems hacky
 -              // to mix READ_LATEST and GAID_FOR_UPDATE, even if they have the same value.
 -              // Maybe deprecate GAID_FOR_UPDATE now that we implement IDBAccessObject?
 -              $id = $this->getArticleID();
 +              $id = $this->getArticleID( $flags );
                if ( $id ) {
                        $fname = __METHOD__;
                        $loadRestrictionsFromDb = function ( IDatabase $dbr ) use ( $fname, $id ) {
                }
  
                $dbr = wfGetDB( DB_REPLICA );
 -              $conds['page_namespace'] = $this->mNamespace;
 +              $conds = [ 'page_namespace' => $this->mNamespace ];
                $conds[] = 'page_title ' . $dbr->buildLike( $this->mDbkeyform . '/', $dbr->anyString() );
                $options = [];
                if ( $limit > -1 ) {
         * Get the article ID for this Title from the link cache,
         * adding it if necessary
         *
 -       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
 -       *  for update
 +       * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return int The ID
         */
        public function getArticleID( $flags = 0 ) {
                if ( $this->mNamespace < 0 ) {
                        $this->mArticleID = 0;
 +
                        return $this->mArticleID;
                }
 +
                $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                if ( $flags & self::GAID_FOR_UPDATE ) {
                        $oldUpdate = $linkCache->forUpdate( true );
                        $linkCache->clearLink( $this );
                        $this->mArticleID = $linkCache->addLinkObj( $this );
                        $linkCache->forUpdate( $oldUpdate );
 +              } elseif ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
 +                      $this->mArticleID = (int)$this->loadFieldFromDB( 'page_id', $flags );
                } elseif ( $this->mArticleID == -1 ) {
                        $this->mArticleID = $linkCache->addLinkObj( $this );
                }
 +
                return $this->mArticleID;
        }
  
         * Is this an article that is a redirect page?
         * Uses link cache, adding it if necessary
         *
 -       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
 +       * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return bool
         */
        public function isRedirect( $flags = 0 ) {
 -              if ( !is_null( $this->mRedirect ) ) {
 -                      return $this->mRedirect;
 -              }
 -              if ( !$this->getArticleID( $flags ) ) {
 -                      $this->mRedirect = false;
 -                      return $this->mRedirect;
 -              }
 +              if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
 +                      $this->mRedirect = (bool)$this->loadFieldFromDB( 'page_is_redirect', $flags );
 +              } else {
 +                      if ( $this->mRedirect !== null ) {
 +                              return $this->mRedirect;
 +                      } elseif ( !$this->getArticleID( $flags ) ) {
 +                              $this->mRedirect = false;
  
 -              $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 -              $linkCache->addLinkObj( $this ); # in case we already had an article ID
 -              $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 -              if ( $cached === null ) {
 -                      # Trust LinkCache's state over our own
 -                      # LinkCache is telling us that the page doesn't exist, despite there being cached
 -                      # data relating to an existing page in $this->mArticleID. Updaters should clear
 -                      # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
 -                      # set, then LinkCache will definitely be up to date here, since getArticleID() forces
 -                      # LinkCache to refresh its data from the master.
 -                      $this->mRedirect = false;
 -                      return $this->mRedirect;
 -              }
 +                              return $this->mRedirect;
 +                      }
  
 -              $this->mRedirect = (bool)$cached;
 +                      $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 +                      $linkCache->addLinkObj( $this ); // in case we already had an article ID
 +                      // Note that LinkCache returns null if it thinks the page does not exist;
 +                      // always trust the state of LinkCache over that of this Title instance.
 +                      $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              }
  
                return $this->mRedirect;
        }
         * What is the length of this page?
         * Uses link cache, adding it if necessary
         *
 -       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
 +       * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return int
         */
        public function getLength( $flags = 0 ) {
 -              if ( $this->mLength != -1 ) {
 -                      return $this->mLength;
 -              }
 -              if ( !$this->getArticleID( $flags ) ) {
 -                      $this->mLength = 0;
 -                      return $this->mLength;
 -              }
 -              $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 -              $linkCache->addLinkObj( $this ); # in case we already had an article ID
 -              $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
 -              if ( $cached === null ) {
 -                      # Trust LinkCache's state over our own, as for isRedirect()
 -                      $this->mLength = 0;
 -                      return $this->mLength;
 -              }
 +              if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
 +                      $this->mLength = (int)$this->loadFieldFromDB( 'page_len', $flags );
 +              } else {
 +                      if ( $this->mLength != -1 ) {
 +                              return $this->mLength;
 +                      } elseif ( !$this->getArticleID( $flags ) ) {
 +                              $this->mLength = 0;
 +                              return $this->mLength;
 +                      }
  
 -              $this->mLength = intval( $cached );
 +                      $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 +                      $linkCache->addLinkObj( $this ); // in case we already had an article ID
 +                      // Note that LinkCache returns null if it thinks the page does not exist;
 +                      // always trust the state of LinkCache over that of this Title instance.
 +                      $this->mLength = (int)$linkCache->getGoodLinkFieldObj( $this, 'length' );
 +              }
  
                return $this->mLength;
        }
        /**
         * What is the page_latest field for this page?
         *
 -       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
 +       * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return int Int or 0 if the page doesn't exist
         */
        public function getLatestRevID( $flags = 0 ) {
 -              if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
 -                      return intval( $this->mLatestID );
 -              }
 -              if ( !$this->getArticleID( $flags ) ) {
 -                      $this->mLatestID = 0;
 -                      return $this->mLatestID;
 -              }
 -              $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 -              $linkCache->addLinkObj( $this ); # in case we already had an article ID
 -              $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
 -              if ( $cached === null ) {
 -                      # Trust LinkCache's state over our own, as for isRedirect()
 -                      $this->mLatestID = 0;
 -                      return $this->mLatestID;
 -              }
 +              if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
 +                      $this->mLatestID = (int)$this->loadFieldFromDB( 'page_latest', $flags );
 +              } else {
 +                      if ( $this->mLatestID !== false ) {
 +                              return (int)$this->mLatestID;
 +                      } elseif ( !$this->getArticleID( $flags ) ) {
 +                              $this->mLatestID = 0;
  
 -              $this->mLatestID = intval( $cached );
 +                              return $this->mLatestID;
 +                      }
 +
 +                      $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 +                      $linkCache->addLinkObj( $this ); // in case we already had an article ID
 +                      // Note that LinkCache returns null if it thinks the page does not exist;
 +                      // always trust the state of LinkCache over that of this Title instance.
 +                      $this->mLatestID = (int)$linkCache->getGoodLinkFieldObj( $this, 'revision' );
 +              }
  
                return $this->mLatestID;
        }
  
        /**
 -       * This clears some fields in this object, and clears any associated
 -       * keys in the "bad links" section of the link cache.
 +       * Inject a page ID, reset DB-loaded fields, and clear the link cache for this title
 +       *
 +       * This can be called on page insertion to allow loading of the new page_id without
 +       * having to create a new Title instance. Likewise with deletion.
         *
 -       * - This is called from WikiPage::doEditContent() and WikiPage::insertOn() to allow
 -       * loading of the new page_id. It's also called from
 -       * WikiPage::doDeleteArticleReal()
 +       * @note This overrides Title::setContentModel()
         *
 -       * @param int $newid The new Article ID
 +       * @param int|bool $id Page ID, 0 for non-existant, or false for "unknown" (lazy-load)
         */
 -      public function resetArticleID( $newid ) {
 -              $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 -              $linkCache->clearLink( $this );
 -
 -              if ( $newid === false ) {
 +      public function resetArticleID( $id ) {
 +              if ( $id === false ) {
                        $this->mArticleID = -1;
                } else {
 -                      $this->mArticleID = intval( $newid );
 +                      $this->mArticleID = (int)$id;
                }
                $this->mRestrictionsLoaded = false;
                $this->mRestrictions = [];
                $this->mLength = -1;
                $this->mLatestID = false;
                $this->mContentModel = false;
 +              $this->mForcedContentModel = false;
                $this->mEstimateRevisions = null;
 -              $this->mPageLanguage = false;
 +              $this->mPageLanguage = null;
                $this->mDbPageLanguage = false;
                $this->mIsBigDeletion = null;
 +
 +              MediaWikiServices::getInstance()->getLinkCache()->clearLink( $this );
        }
  
        public static function clearCaches() {
                //        splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
                /** @var MediaWikiTitleCodec $titleCodec */
                $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
 +              '@phan-var MediaWikiTitleCodec $titleCodec';
                // MalformedTitleException can be thrown here
                $parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace );
  
  
                $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt );
                $method = $auth ? 'moveIfAllowed' : 'move';
 +              /** @var Status $status */
                $status = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
                if ( $status->isOK() ) {
                        return true;
  
                $mp = new MovePage( $this, $nt );
                $method = $auth ? 'moveSubpagesIfAllowed' : 'moveSubpages';
 +              /** @var Status $result */
                $result = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
  
 -              if ( !$result->isOk() ) {
 +              if ( !$result->isOK() ) {
                        return $result->getErrorsArray();
                }
  
                $retval = [];
                foreach ( $result->getValue() as $key => $status ) {
 +                      /** @var Status $status */
                        if ( $status->isOK() ) {
                                $retval[$key] = $status->getValue();
                        } else {
        }
  
        /**
 -       * Checks if this page is just a one-rev redirect.
 -       * Adds lock, so don't use just for light purposes.
 +       * Locks the page row and check if this page is single revision redirect
 +       *
 +       * This updates the cached fields of this instance via Title::loadFromRow()
         *
         * @return bool
         */
        /**
         * Get next/previous revision ID relative to another revision ID
         * @param int $revId Revision ID. Get the revision that was before this one.
 -       * @param int $flags Title::GAID_FOR_UPDATE
 +       * @param int $flags Bitfield of class READ_* constants
         * @param string $dir 'next' or 'prev'
         * @return int|bool New revision ID, or false if none exists
         */
        private function getRelativeRevisionID( $revId, $flags, $dir ) {
                $rl = MediaWikiServices::getInstance()->getRevisionLookup();
 -              $rlFlags = $flags === self::GAID_FOR_UPDATE ? IDBAccessObject::READ_LATEST : 0;
 -              $rev = $rl->getRevisionById( $revId, $rlFlags );
 +              $rev = $rl->getRevisionById( $revId, $flags );
                if ( !$rev ) {
                        return false;
                }
 -              $oldRev = $dir === 'next'
 -                      ? $rl->getNextRevision( $rev, $rlFlags )
 -                      : $rl->getPreviousRevision( $rev, $rlFlags );
 -              if ( !$oldRev ) {
 -                      return false;
 -              }
 -              return $oldRev->getId();
 +
 +              $oldRev = ( $dir === 'next' )
 +                      ? $rl->getNextRevision( $rev, $flags )
 +                      : $rl->getPreviousRevision( $rev, $flags );
 +
 +              return $oldRev ? $oldRev->getId() : false;
        }
  
        /**
         *
         * @deprecated since 1.34, use RevisionLookup::getPreviousRevision
         * @param int $revId Revision ID. Get the revision that was before this one.
 -       * @param int $flags Title::GAID_FOR_UPDATE
 +       * @param int $flags Bitfield of class READ_* constants
         * @return int|bool Old revision ID, or false if none exists
         */
        public function getPreviousRevisionID( $revId, $flags = 0 ) {
         *
         * @deprecated since 1.34, use RevisionLookup::getNextRevision
         * @param int $revId Revision ID. Get the revision that was after this one.
 -       * @param int $flags Title::GAID_FOR_UPDATE
 +       * @param int $flags Bitfield of class READ_* constants
         * @return int|bool Next revision ID, or false if none exists
         */
        public function getNextRevisionID( $revId, $flags = 0 ) {
        /**
         * Get the first revision of the page
         *
 -       * @param int $flags Title::GAID_FOR_UPDATE
 +       * @param int $flags Bitfield of class READ_* constants
         * @return Revision|null If page doesn't exist
         */
        public function getFirstRevision( $flags = 0 ) {
                $pageId = $this->getArticleID( $flags );
                if ( $pageId ) {
 -                      $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
 +                      $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
 +                      list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
                        $revQuery = Revision::getQueryInfo();
 -                      $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
 +                      $row = wfGetDB( $index )->selectRow(
 +                              $revQuery['tables'], $revQuery['fields'],
                                [ 'rev_page' => $pageId ],
                                __METHOD__,
 -                              [
 -                                      'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
 -                                      'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
 -                              ],
 +                              array_merge(
 +                                      [
 +                                              'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
 +                                              'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
 +                                      ],
 +                                      $options
 +                              ),
                                $revQuery['joins']
                        );
                        if ( $row ) {
        /**
         * Get the oldest revision timestamp of this page
         *
 -       * @param int $flags Title::GAID_FOR_UPDATE
 +       * @param int $flags Bitfield of class READ_* constants
         * @return string|null MW timestamp
         */
        public function getEarliestRevTime( $flags = 0 ) {
         * If you want to know if a title can be meaningfully viewed, you should
         * probably call the isKnown() method instead.
         *
 -       * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
 -       *   from master/for update
 +       * @param int $flags Either a bitfield of class READ_* constants or GAID_FOR_UPDATE
         * @return bool
         */
        public function exists( $flags = 0 ) {
         * on the number of links. Typically called on create and delete.
         */
        public function touchLinks() {
 -              DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) );
 +              $jobs = [];
 +              $jobs[] = HTMLCacheUpdateJob::newForBacklinks(
 +                      $this,
 +                      'pagelinks',
 +                      [ 'causeAction' => 'page-touch' ]
 +              );
                if ( $this->mNamespace == NS_CATEGORY ) {
 -                      DeferredUpdates::addUpdate(
 -                              new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' )
 +                      $jobs[] = HTMLCacheUpdateJob::newForBacklinks(
 +                              $this,
 +                              'categorylinks',
 +                              [ 'causeAction' => 'category-touch' ]
                        );
                }
 +
 +              JobQueueGroup::singleton()->lazyPush( $jobs );
        }
  
        /**
                return $notices;
        }
  
 +      /**
 +       * @param int $flags Bitfield of class READ_* constants
 +       * @return string|bool
 +       */
 +      private function loadFieldFromDB( $field, $flags ) {
 +              if ( !in_array( $field, self::getSelectFields(), true ) ) {
 +                      return false; // field does not exist
 +              }
 +
 +              $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
 +              list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
 +
 +              return wfGetDB( $index )->selectField(
 +                      'page',
 +                      $field,
 +                      $this->pageCond(),
 +                      __METHOD__,
 +                      $options
 +              );
 +      }
 +
        /**
         * @return array
         */
@@@ -150,6 -150,9 +150,6 @@@ class TitleTest extends MediaWikiTestCa
                                ]
                        ]
                ] );
 -
 -              // Reset services since we modified $wgLocalInterwikis
 -              $this->overrideMwServices();
        }
  
        /**
                $this->assertEquals(
                        false,
                        $title->exists(),
 -                      'exists() should rely on link cache unless GAID_FOR_UPDATE is used'
 +                      'exists() should rely on link cache unless READ_LATEST is used'
                );
                $this->assertEquals(
                        true,
 -                      $title->exists( Title::GAID_FOR_UPDATE ),
 -                      'exists() should re-query database when GAID_FOR_UPDATE is used'
 +                      $title->exists( Title::READ_LATEST ),
 +                      'exists() should re-query database when READ_LATEST is used'
                );
        }
  
                return [
                        [ Title::makeTitle( NS_SPECIAL, 'Test' ) ],
                        [ Title::makeTitle( NS_MEDIA, 'Test' ) ],
-                       [ Title::makeTitle( NS_MAIN, '', 'Kittens' ) ],
-                       [ Title::makeTitle( NS_MAIN, 'Kittens', '', 'acme' ) ],
+               ];
+       }
+       public static function provideGetTalkPage_broken() {
+               // These cases *should* be bad, but are not treated as bad, for backwards compatibility.
+               // See discussion on T227817.
+               return [
+                       [
+                               Title::makeTitle( NS_MAIN, '', 'Kittens' ),
+                               Title::makeTitle( NS_TALK, '' ), // Section is lost!
+                               false,
+                       ],
+                       [
+                               Title::makeTitle( NS_MAIN, 'Kittens', '', 'acme' ),
+                               Title::makeTitle( NS_TALK, 'Kittens', '' ), // Interwiki prefix is lost!
+                               true,
+                       ],
                ];
        }
  
                $title->getTalkPage();
        }
  
+       /**
+        * @dataProvider provideGetTalkPage_broken
+        * @covers Title::getTalkPageIfDefined
+        */
+       public function testGetTalkPage_broken( Title $title, Title $expected, $valid ) {
+               $errorLevel = error_reporting( E_ERROR );
+               // NOTE: Eventually we want to throw in this case. But while there is still code that
+               // calls this method without checking, we want to avoid fatal errors.
+               // See discussion on T227817.
+               $result = $title->getTalkPage();
+               $this->assertTrue( $expected->equals( $result ) );
+               $this->assertSame( $valid, $result->isValid() );
+               error_reporting( $errorLevel );
+       }
        /**
         * @dataProvider provideGetTalkPage_good
         * @covers Title::getTalkPageIfDefined