diff --git a/includes/Title.php b/includes/Title.php index 048273d6973..dcb95af64c8 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -508,7 +508,7 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { /** * Returns a list of fields that are to be selected for initializing Title - * objects or LinkCache entries. + * objects. * * @deprecated since 1.36, use PageStore::newSelectQueryBuilder() instead. * @@ -1069,6 +1069,16 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { return $this->mNamespace; } + /** + * @param int $flags + * + * @return bool Whether $flags indicates that the latest information should be + * read from the primary database, bypassing caches. + */ + private function shouldReadLatest( int $flags ) { + return ( $flags & ( self::READ_LATEST | self::GAID_FOR_UPDATE ) ) > 0; + } + /** * Get the page's content model id, see the CONTENT_MODEL_XXX constants. * @@ -1086,15 +1096,8 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { 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->lazyFillContentModel( $linkCache->getGoodLinkFieldObj( $this, 'model' ) ); + if ( $this->shouldReadLatest( $flags ) || !$this->mContentModel ) { + $this->lazyFillContentModel( $this->getFieldFromPageStore( 'page_content_model', $flags ) ); } if ( !$this->mContentModel ) { @@ -2860,25 +2863,14 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { * @return int The ID */ public function getArticleID( $flags = 0 ) { - if ( !$this->canExist() ) { + if ( $this->mArticleID === -1 && !$this->canExist() ) { $this->mArticleID = 0; return $this->mArticleID; } - if ( $flags & self::GAID_FOR_UPDATE ) { - $linkCache = MediaWikiServices::getInstance()->getLinkCache(); - $linkCache->clearLink( $this ); - $this->mArticleID = $linkCache->addLinkObj( $this, self::READ_LATEST ); - } elseif ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { - // If mArticleID is >0, pageCond() will use it, making it impossible - // for the call below to return a different result, e.g. after a - // page move. - $this->mArticleID = -1; - $this->mArticleID = (int)$this->loadFieldFromDB( 'page_id', $flags ); - } elseif ( $this->mArticleID == -1 ) { - $linkCache = MediaWikiServices::getInstance()->getLinkCache(); - $this->mArticleID = $linkCache->addLinkObj( $this ); + if ( $this->mArticleID === -1 || $this->shouldReadLatest( $flags ) ) { + $this->mArticleID = (int)$this->getFieldFromPageStore( 'page_id', $flags ); } return $this->mArticleID; @@ -2899,18 +2891,8 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { * @return bool */ public function isRedirect( $flags = 0 ) { - if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) { - $this->mRedirect = (bool)$this->loadFieldFromDB( 'page_is_redirect', $flags ); - } elseif ( $this->mRedirect === null ) { - if ( $this->getArticleID( $flags ) ) { - $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' ); - } else { - $this->mRedirect = false; - } + if ( $this->shouldReadLatest( $flags ) || $this->mRedirect === null ) { + $this->mRedirect = (bool)$this->getFieldFromPageStore( 'page_is_redirect', $flags ); } return $this->mRedirect; @@ -2924,21 +2906,12 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { * @return int */ public function getLength( $flags = 0 ) { - 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; - } + if ( $this->shouldReadLatest( $flags ) || $this->mLength < 0 ) { + $this->mLength = (int)$this->getFieldFromPageStore( 'page_len', $flags ); + } - $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' ); + if ( $this->mLength < 0 ) { + $this->mLength = 0; } return $this->mLength; @@ -2951,22 +2924,12 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { * @return int Int or 0 if the page doesn't exist */ public function getLatestRevID( $flags = 0 ) { - 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; + if ( $this->shouldReadLatest( $flags ) || $this->mLatestID === false ) { + $this->mLatestID = (int)$this->getFieldFromPageStore( 'page_latest', $flags ); + } - 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' ); + if ( !$this->mLatestID ) { + $this->mLatestID = 0; } return $this->mLatestID; @@ -3384,13 +3347,16 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { } /** - * Check if this is a new page + * Check if this is a new page. + * + * @note This returns false if the page does not exist. + * @param int $flags one of the READ_XXX constants. * * @return bool */ - public function isNewPage() { - $dbr = wfGetDB( DB_REPLICA ); - return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ ); + public function isNewPage( $flags = self::READ_NORMAL ) { + // NOTE: we rely on PHP casting "0" to false here. + return (bool)$this->getFieldFromPageStore( 'page_is_new', $flags ); } /** @@ -3757,15 +3723,24 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { /** * Get the last touched timestamp * - * @param IDatabase|null $db + * @param int $flags one of the READ_XXX constants. For historical reasons, an IDatabase + * instance is also accepted here. If an IDatabase is passed, a deprecation warning + * is triggered, caches will be bypassed, and the primary database connection will be + * used. However, the IDatabase instance itself will be ignored. * @return string|false Last-touched timestamp */ - public function getTouched( $db = null ) { - if ( $db === null ) { - $db = wfGetDB( DB_REPLICA ); + public function getTouched( $flags = self::READ_NORMAL ) { + if ( is_object( $flags ) ) { + wfDeprecatedMsg( + __METHOD__ . ' was called with a ' . get_class( $flags ) + . ' instance instead of an integer!', + '1.38' + ); + $flags = self::READ_LATEST; } - $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ ); - return $touched; + + $touched = $this->getFieldFromPageStore( 'page_touched', $flags ); + return MWTimestamp::convert( TS_MW, $touched ); } /** @@ -3938,20 +3913,20 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { * to true in LocalSettings.php, otherwise returns false. If there is no language saved in * the db, it will return NULL. * - * @return string|null|bool + * @param int $flags + * + * @return ?string */ - private function getDbPageLanguageCode() { + private function getDbPageLanguageCode( int $flags = 0 ): ?string { global $wgPageLanguageUseDB; // 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 + // the value is not requested already, lookup the page language using PageStore if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) { - $linkCache = MediaWikiServices::getInstance()->getLinkCache(); - $linkCache->addLinkObj( $this ); - $this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' ); + $this->mDbPageLanguage = $this->getFieldFromPageStore( 'page_lang', $flags ); } - return $this->mDbPageLanguage; + return $this->mDbPageLanguage ?: null; } /** @@ -4124,23 +4099,44 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { /** * @param string $field * @param int $flags Bitfield of class READ_* constants - * @return string|bool + * @return string|false */ - private function loadFieldFromDB( $field, $flags ) { - if ( !in_array( $field, self::getSelectFields(), true ) ) { - return false; // field does not exist + private function getFieldFromPageStore( $field, $flags ) { + $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c + + $pageStore = MediaWikiServices::getInstance()->getPageStore(); + + if ( !in_array( $field, $pageStore->getSelectFields(), true ) ) { + throw new InvalidArgumentException( "Unknown field: $field" ); } - $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c - list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags ); + if ( $flags === self::READ_NORMAL && $this->mArticleID === 0 ) { + // page does not exist + return false; + } - return wfGetDB( $index )->selectField( - 'page', - $field, - $this->pageCond(), - __METHOD__, - $options - ); + if ( !$this->canExist() ) { + return false; + } + + if ( $this->mArticleID > 0 && $field !== 'page_id' ) { + // NOTE: if we already have a page ID, we trust it. + $page = $pageStore->getPageById( $this->getArticleID(), $flags ); + } elseif ( $field === 'page_id' ) { + // NOTE: When looking up the page ID, don't use PageStore::getPageByReference(). + // getPageByReference() would call exists() and getId(), which would land + // us back here, recursing until we run out of stack. + $page = $pageStore->getPageByName( $this->getNamespace(), $this->getDBkey(), $flags ); + } else { + $page = $pageStore->getPageByReference( $this, $flags ); + } + + if ( $page instanceof PageStoreRecord ) { + return $page->getField( $field ); + } else { + // page does not exist + return false; + } } /** @@ -4277,10 +4273,10 @@ class Title implements LinkTarget, PageIdentity, IDBAccessObject { 'page_title' => $this->getDBkey(), 'page_wiki_id' => $this->getWikiId(), 'page_latest' => $this->getLatestRevID( $flags ), - 'page_is_new' => $this->isNewPage(), // no flags? + 'page_is_new' => $this->isNewPage( $flags ), 'page_is_redirect' => $this->isRedirect( $flags ), - 'page_touched' => $this->getTouched(), // no flags? - 'page_lang' => $this->getDbPageLanguageCode() ?: null, + 'page_touched' => $this->getTouched( $flags ), + 'page_lang' => $this->getDbPageLanguageCode( $flags ), ], PageIdentity::LOCAL ); diff --git a/includes/page/PageLookup.php b/includes/page/PageLookup.php index 3dd0f70ba22..c9eb69c38c6 100644 --- a/includes/page/PageLookup.php +++ b/includes/page/PageLookup.php @@ -103,6 +103,7 @@ interface PageLookup extends IDBAccessObject { /** * Returns the PageRecord of the given page. * May return $page if that already is a PageRecord. + * If $page is a PageIdentity, implementations may call methods like exists() and getId() on it. * * The PageReference must refer to a proper page - that is, it must not refer to a special page. * diff --git a/includes/page/PageStore.php b/includes/page/PageStore.php index 9a7a805f462..428cfa36c85 100644 --- a/includes/page/PageStore.php +++ b/includes/page/PageStore.php @@ -324,6 +324,9 @@ class PageStore implements PageLookup { // if we have a page ID, use it $id = $page->getId( $this->wikiId ); return $this->getPageById( $id, $queryFlags ); + } elseif ( $queryFlags === self::READ_NORMAL ) { + // The page does not appear to exist, and we don't have to check again. + return null; } } diff --git a/includes/page/PageStoreRecord.php b/includes/page/PageStoreRecord.php index 8123196e1de..be34357e57c 100644 --- a/includes/page/PageStoreRecord.php +++ b/includes/page/PageStoreRecord.php @@ -119,8 +119,25 @@ class PageStoreRecord extends PageIdentityValue implements ExistingPageRecord { * @return ?string */ public function getLanguage(): ?string { - // field may be missing - return $this->row->page_lang ?? null; + return $this->getField( 'page_lang' ); + } + + /** + * Return the raw value for the given field as returned by the database query. + * + * Numeric values may be encoded as strings. + * Boolean values may be represented as integers (or numeric strings). + * Timestamps will use the database's native format. + * + * @internal + * + * @param string $field + * + * @return string|int|bool|null + */ + public function getField( string $field ) { + // Field may be missing entirely. + return $this->row->$field ?? null; } } diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index 4cb89ede5e1..6c24cd3c803 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -516,6 +516,7 @@ class TitleTest extends MediaWikiIntegrationTestCase { $this->assertSame( MWTimestamp::convert( TS_MW, $title->getTouched() ), $record->getTouched() ); $this->assertSame( $title->isNewPage(), $record->isNew() ); $this->assertSame( $title->isRedirect(), $record->isRedirect() ); + $this->assertSame( $title->getTouched(), $record->getTouched() ); } /** diff --git a/tests/phpunit/unit/includes/page/PageStoreRecordTest.php b/tests/phpunit/unit/includes/page/PageStoreRecordTest.php index 13c5a510547..67d0a5664ea 100644 --- a/tests/phpunit/unit/includes/page/PageStoreRecordTest.php +++ b/tests/phpunit/unit/includes/page/PageStoreRecordTest.php @@ -100,6 +100,10 @@ class PageStoreRecordTest extends MediaWikiUnitTestCase { $this->assertSame( $row->page_is_redirect, $pageRecord->isRedirect() ); $this->assertSame( $row->page_lang ?? null, $pageRecord->getLanguage() ); + + foreach ( $row as $name => $value ) { + $this->assertEquals( $value, $pageRecord->getField( $name ) ); + } } public function badConstructorProvider() {