X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FStorage%2FRevisionStore.php;h=2e3103c527a8d29f2a7ac696f68dd889a2daeb02;hb=ff35ae61958412f22e79466a6dbab95c4d2bebd7;hp=79ecec691f15e687b5abdabdd27a630213c6a58a;hpb=652b84c0fe193ba1f3f6f9b11bba1a39cdfe8ca6;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Storage/RevisionStore.php b/includes/Storage/RevisionStore.php index 79ecec691f..2e3103c527 100644 --- a/includes/Storage/RevisionStore.php +++ b/includes/Storage/RevisionStore.php @@ -26,6 +26,7 @@ namespace MediaWiki\Storage; +use ActorMigration; use CommentStore; use CommentStoreComment; use Content; @@ -42,6 +43,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 +65,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 +93,37 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup */ private $cache; + /** + * @var CommentStore + */ + private $commentStore; + + /** + * @var ActorMigration + */ + private $actorMigration; + + /** + * @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 ActorMigration $actorMigration * @param bool|string $wikiId */ public function __construct( LoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, + CommentStore $commentStore, + ActorMigration $actorMigration, $wikiId = false ) { Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' ); @@ -107,7 +131,21 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $this->loadBalancer = $loadBalancer; $this->blobStore = $blobStore; $this->cache = $cache; + $this->commentStore = $commentStore; + $this->actorMigration = $actorMigration; $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(); } /** @@ -173,23 +211,34 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * @return Title * @throws RevisionAccessException */ - public 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' ); } - list( $dbMode, $dbOptions, , ) = DBAccessObjectUtils::getDBOptions( $queryFlags ); - $titleFlags = $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0; - $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, $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 ) { + $canUseRevId = ( $revId !== null && $revId > 0 ); + + if ( $canUseRevId ) { $dbr = $this->getDBConnectionRef( $dbMode ); // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that $row = $dbr->selectRow( @@ -209,17 +258,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" + ); } /** @@ -257,8 +314,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 * @@ -340,14 +397,16 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' ); $timestamp = $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' ); + // Checks. + $this->failOnNull( $user->getId(), 'user field' ); + $this->failOnEmpty( $user->getName(), 'user_text field' ); + # Record the edit in revisions $row = [ 'rev_page' => $pageId, 'rev_parent_id' => $parentId, 'rev_text_id' => $textId, 'rev_minor_edit' => $rev->isMinor() ? 1 : 0, - 'rev_user' => $this->failOnNull( $user->getId(), 'user field' ), - 'rev_user_text' => $this->failOnEmpty( $user->getName(), 'user_text field' ), 'rev_timestamp' => $dbw->timestamp( $timestamp ), 'rev_deleted' => $rev->getVisibility(), 'rev_len' => $size, @@ -360,9 +419,13 @@ 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; + list( $actorFields, $actorCallback ) = + $this->actorMigration->getInsertValuesWithTempTable( $dbw, 'rev_user', $user ); + $row += $actorFields; + if ( $this->contentHandlerUseDB ) { // MCR migration note: rev_content_model and rev_content_format will go away @@ -380,22 +443,21 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $row['rev_id'] = intval( $dbw->insertId() ); } $commentCallback( $row['rev_id'] ); + $actorCallback( $row['rev_id'], $row ); // Insert IP revision into ip_changes for use when querying for a range. - if ( $row['rev_user'] === 0 && IP::isValid( $row['rev_user_text'] ) ) { + if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) { $ipcRow = [ 'ipc_rev_id' => $row['rev_id'], 'ipc_rev_timestamp' => $row['rev_timestamp'], - 'ipc_hex' => IP::toHex( $row['rev_user_text'] ), + 'ipc_hex' => IP::toHex( $user->getName() ), ]; $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ ); } - $newSlot = SlotRecord::newSaved( $row['rev_id'], $blobAddress, $slot ); + $newSlot = SlotRecord::newSaved( $row['rev_id'], $textId, $blobAddress, $slot ); $slots = new RevisionSlots( [ 'main' => $newSlot ] ); - $user = new UserIdentityValue( intval( $row['rev_user'] ), $row['rev_user_text'] ); - $rev = new RevisionStoreRecord( $title, $user, @@ -535,6 +597,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup 'page' => $title->getArticleID(), 'user_text' => $user->getName(), 'user' => $user->getId(), + 'actor' => $user->getActorId(), 'comment' => $comment, 'minor_edit' => $minor, 'text_id' => $current->rev_text_id, @@ -550,7 +613,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $fields['title'] = Title::makeTitle( $current->page_namespace, $current->page_title ); - $mainSlot = $this->emulateMainSlot_1_29( $fields, 0, $title ); + $mainSlot = $this->emulateMainSlot_1_29( $fields, self::READ_LATEST, $title ); $revision = new MutableRevisionRecord( $title, $this->wikiId ); $this->initializeMutableRevisionFromArray( $revision, $fields ); $revision->setSlot( $mainSlot ); @@ -606,9 +669,10 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup } // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that! + $actorWhere = $this->actorMigration->getWhere( $dbr, 'rc_user', $rev->getUser(), false ); $rc = RecentChange::newFromConds( [ - 'rc_user_text' => $userIdentity->getName(), + $actorWhere['conds'], 'rc_timestamp' => $dbr->timestamp( $rev->getTimestamp() ), 'rc_this_oldid' => $rev->getId() ], @@ -643,6 +707,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup 'ar_timestamp' => 'rev_timestamp', 'ar_user_text' => 'rev_user_text', 'ar_user' => 'rev_user', + 'ar_actor' => 'rev_actor', 'ar_minor_edit' => 'rev_minor_edit', 'ar_deleted' => 'rev_deleted', 'ar_len' => 'rev_len', @@ -686,6 +751,10 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) { $mainSlotRow = new stdClass(); $mainSlotRow->role_name = 'main'; + $mainSlotRow->model_name = null; + $mainSlotRow->slot_revision_id = null; + $mainSlotRow->content_address = null; + $mainSlotRow->slot_content_id = null; $content = null; $blobData = null; @@ -693,14 +762,13 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup if ( is_object( $row ) ) { // archive row - if ( !isset( $row->rev_id ) && isset( $row->ar_user ) ) { + if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) { $row = $this->mapArchiveFields( $row ); } 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; + $mainSlotRow->slot_content_id = $row->rev_text_id; + $mainSlotRow->content_address = 'tt:' . $row->rev_text_id; } if ( isset( $row->old_text ) ) { @@ -713,10 +781,10 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $blobFlags = ( $row->old_flags === null ) ? '' : $row->old_flags; } - $mainSlotRow->slot_revision = intval( $row->rev_id ); + $mainSlotRow->slot_revision_id = intval( $row->rev_id ); - $mainSlotRow->cont_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null; - $mainSlotRow->cont_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null; + $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null; + $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null; $mainSlotRow->model_name = isset( $row->rev_content_model ) ? strval( $row->rev_content_model ) : null; @@ -725,13 +793,16 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup ? strval( $row->rev_content_format ) : null; } elseif ( is_array( $row ) ) { - $mainSlotRow->slot_revision = isset( $row['id'] ) ? intval( $row['id'] ) : null; + $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null; - $mainSlotRow->cont_address = isset( $row['text_id'] ) + $mainSlotRow->slot_content_id = isset( $row['text_id'] ) + ? intval( $row['text_id'] ) + : null; + $mainSlotRow->content_address = isset( $row['text_id'] ) ? 'tt:' . intval( $row['text_id'] ) : null; - $mainSlotRow->cont_size = isset( $row['len'] ) ? intval( $row['len'] ) : null; - $mainSlotRow->cont_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; + $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null; + $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null; $mainSlotRow->model_name = isset( $row['content_model'] ) ? strval( $row['content_model'] ) : null; // XXX: must be a string! @@ -790,6 +861,7 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup }; } + $mainSlotRow->slot_id = $mainSlotRow->slot_revision_id; return new SlotRecord( $mainSlotRow, $content ); } @@ -972,9 +1044,10 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup * @return RevisionRecord|null */ public function getRevisionByTimestamp( $title, $timestamp ) { + $db = $this->getDBConnection( DB_REPLICA ); return $this->newRevisionFromConds( [ - 'rev_timestamp' => $timestamp, + 'rev_timestamp' => $db->timestamp( $timestamp ), 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ], @@ -1034,11 +1107,20 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $row->$field = $value; } - $user = $this->getUserIdentityFromRowObject( $row, 'ar_' ); + try { + $user = User::newFromAnyId( + isset( $row->ar_user ) ? $row->ar_user : null, + isset( $row->ar_user_text ) ? $row->ar_user_text : null, + isset( $row->ar_actor ) ? $row->ar_actor : null + ); + } catch ( InvalidArgumentException $ex ) { + wfWarn( __METHOD__ . ': ' . $ex->getMessage() ); + $user = new UserIdentityValue( 0, '', 0 ); + } - $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 ] ); @@ -1046,34 +1128,6 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId ); } - /** - * @param object $row - * @param string $prefix Field prefix, such as 'rev_' or 'ar_'. - * - * @return UserIdentityValue - */ - private function getUserIdentityFromRowObject( $row, $prefix = 'rev_' ) { - $idField = "{$prefix}user"; - $nameField = "{$prefix}user_text"; - - $userId = intval( $row->$idField ); - - if ( isset( $row->user_name ) ) { - $userName = $row->user_name; - } elseif ( isset( $row->$nameField ) ) { - $userName = $row->$nameField; - } else { - $userName = User::whoIs( $userId ); - } - - if ( $userName === false ) { - wfWarn( __METHOD__ . ': Cannot determine user name for user ID ' . $userId ); - $userName = ''; - } - - return new UserIdentityValue( $userId, $userName ); - } - /** * @see RevisionFactory::newRevisionFromRow_1_29 * @@ -1104,11 +1158,20 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup } } - $user = $this->getUserIdentityFromRowObject( $row ); + try { + $user = User::newFromAnyId( + isset( $row->rev_user ) ? $row->rev_user : null, + isset( $row->rev_user_text ) ? $row->rev_user_text : null, + isset( $row->rev_actor ) ? $row->rev_actor : null + ); + } catch ( InvalidArgumentException $ex ) { + wfWarn( __METHOD__ . ': ' . $ex->getMessage() ); + $user = new UserIdentityValue( 0, '', 0 ); + } - $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 ] ); @@ -1183,27 +1246,6 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup } } - // Replaces old lazy loading logic in Revision::getUserText. - if ( !isset( $fields['user_text'] ) && isset( $fields['user'] ) ) { - if ( $fields['user'] instanceof UserIdentity ) { - /** @var User $user */ - $user = $fields['user']; - $fields['user_text'] = $user->getName(); - $fields['user'] = $user->getId(); - } else { - // TODO: wrap this in a callback to make it lazy again. - $name = $fields['user'] === 0 ? false : User::whoIs( $fields['user'] ); - - if ( $name === false ) { - throw new MWException( - 'user_text not given, and unknown user ID ' . $fields['user'] - ); - } - - $fields['user_text'] = $name; - } - } - if ( isset( $fields['comment'] ) && !( $fields['comment'] instanceof CommentStoreComment ) @@ -1246,16 +1288,15 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) { $user = $fields['user']; - } elseif ( isset( $fields['user'] ) && isset( $fields['user_text'] ) ) { - $user = new UserIdentityValue( intval( $fields['user'] ), $fields['user_text'] ); - } elseif ( isset( $fields['user'] ) ) { - $user = User::newFromId( intval( $fields['user'] ) ); - } elseif ( isset( $fields['user_text'] ) ) { - $user = User::newFromName( $fields['user_text'] ); - - // User::newFromName will return false for IP addresses (and invalid names) - if ( $user == false ) { - $user = new UserIdentityValue( 0, $fields['user_text'] ); + } else { + try { + $user = User::newFromAnyId( + isset( $fields['user'] ) ? $fields['user'] : null, + isset( $fields['user_text'] ) ? $fields['user_text'] : null, + isset( $fields['actor'] ) ? $fields['actor'] : null + ); + } catch ( InvalidArgumentException $ex ) { + $user = null; } } @@ -1572,8 +1613,6 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup 'rev_page', 'rev_text_id', 'rev_timestamp', - 'rev_user_text', - 'rev_user', 'rev_minor_edit', 'rev_deleted', 'rev_len', @@ -1581,11 +1620,16 @@ 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'] ); + $actorQuery = $this->actorMigration->getJoin( 'rev_user' ); + $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] ); + $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] ); + $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] ); + if ( $this->contentHandlerUseDB ) { $ret['fields'][] = 'rev_content_format'; $ret['fields'][] = 'rev_content_model'; @@ -1609,7 +1653,8 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup $ret['fields'] = array_merge( $ret['fields'], [ 'user_name', ] ); - $ret['joins']['user'] = [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ]; + $u = $actorQuery['fields']['rev_user']; + $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ]; } if ( in_array( 'text', $options, true ) ) { @@ -1638,9 +1683,10 @@ 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' ); + $actorQuery = $this->actorMigration->getJoin( 'ar_user' ); $ret = [ - 'tables' => [ 'archive' ] + $commentQuery['tables'], + 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'], 'fields' => [ 'ar_id', 'ar_page_id', @@ -1650,15 +1696,13 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup 'ar_text', 'ar_text_id', 'ar_timestamp', - 'ar_user_text', - 'ar_user', 'ar_minor_edit', 'ar_deleted', 'ar_len', 'ar_parent_id', 'ar_sha1', - ] + $commentQuery['fields'], - 'joins' => $commentQuery['joins'], + ] + $commentQuery['fields'] + $actorQuery['fields'], + 'joins' => $commentQuery['joins'] + $actorQuery['joins'], ]; if ( $this->contentHandlerUseDB ) { @@ -1881,15 +1925,19 @@ class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup return false; } + $revQuery = self::getQueryInfo(); $res = $db->select( - 'revision', - 'rev_user', + $revQuery['tables'], + [ + 'rev_user' => $revQuery['fields']['rev_user'], + ], [ 'rev_page' => $pageId, 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) ) ], __METHOD__, - [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] + [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ], + $revQuery['joins'] ); foreach ( $res as $row ) { if ( $row->rev_user != $userId ) {