X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FStorage%2FRevisionStore.php;h=6c8cac94232e1b7dde7e323f63851a4220143d87;hb=d9ba7cd0050d531c4f016fda285793568fa133c7;hp=ce56efceaa0c4169e583f10851b8893b39c1c777;hpb=7040be4f72b46a0052f8896ef462d8b98f45e157;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Storage/RevisionStore.php b/includes/Storage/RevisionStore.php index ce56efceaa..db06afee7e 100644 --- a/includes/Storage/RevisionStore.php +++ b/includes/Storage/RevisionStore.php @@ -42,6 +42,9 @@ use MediaWiki\User\UserIdentityValue; use Message; use MWException; use MWUnknownContentModelException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use RecentChange; use stdClass; use Title; @@ -61,7 +64,8 @@ use Wikimedia\Rdbms\LoadBalancer; * @note This was written to act as a drop-in replacement for the corresponding * static methods in Revision. */ -class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup { +class RevisionStore + implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface { /** * @var SqlBlobStore @@ -88,18 +92,30 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup */ private $cache; + /** + * @var CommentStore + */ + private $commentStore; + + /** + * @var LoggerInterface + */ + private $logger; + /** * @todo $blobStore should be allowed to be any BlobStore! * * @param LoadBalancer $loadBalancer * @param SqlBlobStore $blobStore * @param WANObjectCache $cache + * @param CommentStore $commentStore * @param bool|string $wikiId */ public function __construct( LoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, + CommentStore $commentStore, $wikiId = false ) { Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' ); @@ -107,7 +123,20 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $this->loadBalancer = $loadBalancer; $this->blobStore = $blobStore; $this->cache = $cache; + $this->commentStore = $commentStore; $this->wikiId = $wikiId; + $this->logger = new NullLogger(); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * @return bool Whether the store is read-only + */ + public function isReadOnly() { + return $this->blobStore->isReadOnly(); } /** @@ -164,6 +193,8 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * * MCR migration note: this corresponds to Revision::getTitle * + * @note this method should be private, external use should be avoided! + * * @param int|null $pageId * @param int|null $revId * @param int $queryFlags @@ -171,24 +202,35 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * @return Title * @throws RevisionAccessException */ - private function getTitle( $pageId, $revId, $queryFlags = 0 ) { + public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) { if ( !$pageId && !$revId ) { throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' ); } - $title = null; + // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title + // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method + if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) { + $queryFlags = self::READ_NORMAL; + } + + $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false ); + list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); + $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 ); // Loading by ID is best, but Title::newFromID does not support that for foreign IDs. - if ( $pageId !== null && $pageId > 0 && $this->wikiId === false ) { + if ( $canUseTitleNewFromId ) { // TODO: better foreign title handling (introduce TitleFactory) - $title = Title::newFromID( $pageId, $queryFlags ); + $title = Title::newFromID( $pageId, $titleFlags ); + if ( $title ) { + return $title; + } } // rev_id is defined as NOT NULL, but this revision may not yet have been inserted. - if ( !$title && $revId !== null && $revId > 0 ) { - list( $dbMode, $dbOptions, , ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); + $canUseRevId = ( $revId !== null && $revId > 0 ); - $dbr = $this->getDbConnectionRef( $dbMode ); + if ( $canUseRevId ) { + $dbr = $this->getDBConnectionRef( $dbMode ); // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that $row = $dbr->selectRow( [ 'revision', 'page' ], @@ -207,17 +249,25 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup ); if ( $row ) { // TODO: better foreign title handling (introduce TitleFactory) - $title = Title::newFromRow( $row ); + return Title::newFromRow( $row ); } } - if ( !$title ) { - throw new RevisionAccessException( - "Could not determine title for page ID $pageId and revision ID $revId" - ); + // If we still don't have a title, fallback to master if that wasn't already happening. + if ( $dbMode !== DB_MASTER ) { + $title = $this->getTitle( $pageId, $revId, self::READ_LATEST ); + if ( $title ) { + $this->logger->info( + __METHOD__ . ' fell back to READ_LATEST and got a Title.', + [ 'trace' => wfBacktrace() ] + ); + return $title; + } } - return $title; + throw new RevisionAccessException( + "Could not determine title for page ID $pageId and revision ID $revId" + ); } /** @@ -255,8 +305,8 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup } /** - * Insert a new revision into the database, returning the new revision ID - * number on success and dies horribly on failure. + * Insert a new revision into the database, returning the new revision record + * on success and dies horribly on failure. * * MCR migration note: this replaces Revision::insertOn * @@ -358,7 +408,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup } list( $commentFields, $commentCallback ) = - CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $comment ); + $this->commentStore->insertWithTempTable( $dbw, 'rev_comment', $comment ); $row += $commentFields; if ( $this->contentHandlerUseDB ) { @@ -562,9 +612,13 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup /** * MCR migration note: this replaces Revision::isUnpatrolled * + * @todo This is overly specific, so move or kill this method. + * + * @param RevisionRecord $rev + * * @return int Rcid of the unpatrolled row, zero if there isn't one */ - public function isUnpatrolled( RevisionRecord $rev ) { + public function getRcIdIfUnpatrolled( RevisionRecord $rev ) { $rc = $this->getRecentChange( $rev ); if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) { return $rc->getAttribute( 'rc_id' ); @@ -683,7 +737,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $content = null; $blobData = null; - $blobFlags = ''; + $blobFlags = null; if ( is_object( $row ) ) { // archive row @@ -693,14 +747,16 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) { $mainSlotRow->cont_address = 'tt:' . $row->rev_text_id; - } elseif ( isset( $row->ar_id ) ) { - $mainSlotRow->cont_address = 'ar:' . $row->ar_id; } if ( isset( $row->old_text ) ) { // this happens when the text-table gets joined directly, in the pre-1.30 schema $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null; - $blobFlags = isset( $row->old_flags ) ? strval( $row->old_flags ) : ''; + // Check against selects that might have not included old_flags + if ( !property_exists( $row, 'old_flags' ) ) { + throw new InvalidArgumentException( 'old_flags was not set in $row' ); + } + $blobFlags = ( $row->old_flags === null ) ? '' : $row->old_flags; } $mainSlotRow->slot_revision = intval( $row->rev_id ); @@ -729,7 +785,9 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $mainSlotRow->format_name = isset( $row['content_format'] ) ? strval( $row['content_format'] ) : null; $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null; - $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : ''; + // XXX: If the flags field is not set then $blobFlags should be null so that no + // decoding will happen. An empty string will result in default decodings. + $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null; // if we have a Content object, override mText and mContentModel if ( !empty( $row['content'] ) ) { @@ -791,7 +849,9 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * * @param SlotRecord $slot The SlotRecord to load content for * @param string|null $blobData The content blob, in the form indicated by $blobFlags - * @param string $blobFlags Flags indicating how $blobData needs to be processed + * @param string|null $blobFlags Flags indicating how $blobData needs to be processed. + * Use null if no processing should happen. That is in constrast to the empty string, + * which causes the blob to be decoded according to the configured legacy encoding. * @param string|null $blobFormat MIME type indicating how $dataBlob is encoded * @param int $queryFlags * @@ -801,23 +861,28 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup private function loadSlotContent( SlotRecord $slot, $blobData = null, - $blobFlags = '', + $blobFlags = null, $blobFormat = null, $queryFlags = 0 ) { if ( $blobData !== null ) { Assert::parameterType( 'string', $blobData, '$blobData' ); - Assert::parameterType( 'string', $blobFlags, '$blobFlags' ); + Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' ); $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null; - $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey ); - - if ( $data === false ) { - throw new RevisionAccessException( - "Failed to expand blob data using flags $blobFlags (key: $cacheKey)" - ); + if ( $blobFlags === null ) { + // No blob flags, so use the blob verbatim. + $data = $blobData; + } else { + $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey ); + if ( $data === false ) { + throw new RevisionAccessException( + "Failed to expand blob data using flags $blobFlags (key: $cacheKey)" + ); + } } + } else { $address = $slot->getAddress(); try { @@ -952,7 +1017,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * @param string $timestamp * @return RevisionRecord|null */ - public function getRevisionFromTimestamp( $title, $timestamp ) { + public function getRevisionByTimestamp( $title, $timestamp ) { return $this->newRevisionFromConds( [ 'rev_timestamp' => $timestamp, @@ -1017,9 +1082,9 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $user = $this->getUserIdentityFromRowObject( $row, 'ar_' ); - $comment = CommentStore::newKey( 'ar_comment' ) + $comment = $this->commentStore // Legacy because $row may have come from self::selectFields() - ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), $row, true ); + ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'ar_comment', $row, true ); $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title ); $slots = new RevisionSlots( [ 'main' => $mainSlot ] ); @@ -1087,9 +1152,9 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $user = $this->getUserIdentityFromRowObject( $row ); - $comment = CommentStore::newKey( 'rev_comment' ) + $comment = $this->commentStore // Legacy because $row may have come from self::selectFields() - ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), $row, true ); + ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'rev_comment', $row, true ); $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title ); $slots = new RevisionSlots( [ 'main' => $mainSlot ] ); @@ -1476,10 +1541,20 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $storeWiki = $storeWiki ?: wfWikiID(); $dbWiki = $dbWiki ?: wfWikiID(); - if ( $dbWiki !== $storeWiki ) { - throw new MWException( "RevisionStore for $storeWiki " - . "cannot be used with a DB connection for $dbWiki" ); + if ( $dbWiki === $storeWiki ) { + return; + } + + // HACK: counteract encoding imposed by DatabaseDomain + $storeWiki = str_replace( '?h', '-', $storeWiki ); + $dbWiki = str_replace( '?h', '-', $dbWiki ); + + if ( $dbWiki === $storeWiki ) { + return; } + + throw new MWException( "RevisionStore for $storeWiki " + . "cannot be used with a DB connection for $dbWiki" ); } /** @@ -1552,7 +1627,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup 'rev_sha1', ] ); - $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin(); + $commentQuery = $this->commentStore->getJoin( 'rev_comment' ); $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] ); $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] ); $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] ); @@ -1609,7 +1684,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` */ public function getArchiveQueryInfo() { - $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin(); + $commentQuery = $this->commentStore->getJoin( 'ar_comment' ); $ret = [ 'tables' => [ 'archive' ] + $commentQuery['tables'], 'fields' => [ @@ -1645,6 +1720,21 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * * MCR migration note: this replaces Revision::getParentLengths * + * @param int[] $revIds + * @return int[] associative array mapping revision IDs from $revIds to the nominal size + * of the corresponding revision. + */ + public function getRevisionSizes( array $revIds ) { + return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds ); + } + + /** + * Do a batched query for the sizes of a set of revisions. + * + * MCR migration note: this replaces Revision::getParentLengths + * + * @deprecated use RevisionStore::getRevisionSizes instead. + * * @param IDatabase $db * @param int[] $revIds * @return int[] associative array mapping revision IDs from $revIds to the nominal size @@ -1678,11 +1768,14 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * MCR migration note: this replaces Revision::getPrevious * * @param RevisionRecord $rev + * @param Title $title if known (optional) * * @return RevisionRecord|null */ - public function getPreviousRevision( RevisionRecord $rev ) { - $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); + public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) { + if ( $title === null ) { + $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); + } $prev = $title->getPreviousRevisionID( $rev->getId() ); if ( $prev ) { return $this->getRevisionByTitle( $title, $prev ); @@ -1696,11 +1789,14 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * MCR migration note: this replaces Revision::getNext * * @param RevisionRecord $rev + * @param Title $title if known (optional) * * @return RevisionRecord|null */ - public function getNextRevision( RevisionRecord $rev ) { - $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); + public function getNextRevision( RevisionRecord $rev, Title $title = null ) { + if ( $title === null ) { + $title = $this->getTitle( $rev->getPageId(), $rev->getId() ); + } $next = $title->getNextRevisionID( $rev->getId() ); if ( $next ) { return $this->getRevisionByTitle( $title, $next );