2017-08-27 15:29:18 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* Service for looking up page revisions.
|
|
|
|
|
*
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* Attribution notice: when this file was created, much of its content was taken
|
|
|
|
|
* from the Revision.php file as present in release 1.30. Refer to the history
|
|
|
|
|
* of that file for original authorship.
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
*/
|
|
|
|
|
|
2018-09-20 17:29:04 +00:00
|
|
|
namespace MediaWiki\Revision;
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2017-09-12 17:12:29 +00:00
|
|
|
use ActorMigration;
|
2017-08-27 15:29:18 +00:00
|
|
|
use CommentStore;
|
|
|
|
|
use CommentStoreComment;
|
|
|
|
|
use Content;
|
|
|
|
|
use ContentHandler;
|
|
|
|
|
use DBAccessObjectUtils;
|
|
|
|
|
use Hooks;
|
2017-12-28 15:30:05 +00:00
|
|
|
use IDBAccessObject;
|
2017-08-27 15:29:18 +00:00
|
|
|
use InvalidArgumentException;
|
2020-01-18 20:25:04 +00:00
|
|
|
use MediaWiki\Content\IContentHandlerFactory;
|
2017-08-27 15:29:18 +00:00
|
|
|
use MediaWiki\Linker\LinkTarget;
|
2018-09-20 17:29:04 +00:00
|
|
|
use MediaWiki\Storage\BlobAccessException;
|
|
|
|
|
use MediaWiki\Storage\BlobStore;
|
|
|
|
|
use MediaWiki\Storage\NameTableStore;
|
|
|
|
|
use MediaWiki\Storage\SqlBlobStore;
|
2017-08-27 15:29:18 +00:00
|
|
|
use MediaWiki\User\UserIdentity;
|
|
|
|
|
use MediaWiki\User\UserIdentityValue;
|
|
|
|
|
use Message;
|
|
|
|
|
use MWException;
|
2020-04-02 20:00:24 +00:00
|
|
|
use MWTimestamp;
|
2017-08-27 15:29:18 +00:00
|
|
|
use MWUnknownContentModelException;
|
2018-01-10 16:05:46 +00:00
|
|
|
use Psr\Log\LoggerAwareInterface;
|
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
|
use Psr\Log\NullLogger;
|
2017-08-27 15:29:18 +00:00
|
|
|
use RecentChange;
|
2018-06-13 18:18:47 +00:00
|
|
|
use Revision;
|
2018-10-04 10:49:50 +00:00
|
|
|
use RuntimeException;
|
2019-08-30 18:26:00 +00:00
|
|
|
use StatusValue;
|
2017-08-27 15:29:18 +00:00
|
|
|
use Title;
|
2019-08-30 18:26:00 +00:00
|
|
|
use Traversable;
|
2017-08-27 15:29:18 +00:00
|
|
|
use User;
|
|
|
|
|
use WANObjectCache;
|
|
|
|
|
use Wikimedia\Assert\Assert;
|
2019-06-25 18:53:15 +00:00
|
|
|
use Wikimedia\IPUtils;
|
2017-08-27 15:29:18 +00:00
|
|
|
use Wikimedia\Rdbms\Database;
|
|
|
|
|
use Wikimedia\Rdbms\DBConnRef;
|
|
|
|
|
use Wikimedia\Rdbms\IDatabase;
|
2018-06-27 12:16:35 +00:00
|
|
|
use Wikimedia\Rdbms\ILoadBalancer;
|
2019-07-04 19:56:31 +00:00
|
|
|
use Wikimedia\Rdbms\IResultWrapper;
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Service for looking up page revisions.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.31
|
2018-09-20 17:29:04 +00:00
|
|
|
* @since 1.32 Renamed from MediaWiki\Storage\RevisionStore
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @note This was written to act as a drop-in replacement for the corresponding
|
|
|
|
|
* static methods in Revision.
|
|
|
|
|
*/
|
2018-01-10 16:05:46 +00:00
|
|
|
class RevisionStore
|
|
|
|
|
implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-01-29 15:54:02 +00:00
|
|
|
const ROW_CACHE_KEY = 'revision-row-1.29';
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
|
|
|
|
* @var SqlBlobStore
|
|
|
|
|
*/
|
|
|
|
|
private $blobStore;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var bool|string
|
|
|
|
|
*/
|
2019-06-27 01:33:18 +00:00
|
|
|
private $dbDomain;
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
/**
|
2018-06-27 12:16:35 +00:00
|
|
|
* @var ILoadBalancer
|
2017-08-27 15:29:18 +00:00
|
|
|
*/
|
|
|
|
|
private $loadBalancer;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var WANObjectCache
|
|
|
|
|
*/
|
|
|
|
|
private $cache;
|
|
|
|
|
|
2018-01-29 14:25:49 +00:00
|
|
|
/**
|
|
|
|
|
* @var CommentStore
|
|
|
|
|
*/
|
|
|
|
|
private $commentStore;
|
|
|
|
|
|
2017-09-12 17:12:29 +00:00
|
|
|
/**
|
|
|
|
|
* @var ActorMigration
|
|
|
|
|
*/
|
|
|
|
|
private $actorMigration;
|
|
|
|
|
|
2018-01-10 16:05:46 +00:00
|
|
|
/**
|
|
|
|
|
* @var LoggerInterface
|
|
|
|
|
*/
|
|
|
|
|
private $logger;
|
|
|
|
|
|
2018-01-29 15:54:02 +00:00
|
|
|
/**
|
|
|
|
|
* @var NameTableStore
|
|
|
|
|
*/
|
|
|
|
|
private $contentModelStore;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var NameTableStore
|
|
|
|
|
*/
|
|
|
|
|
private $slotRoleStore;
|
|
|
|
|
|
2018-11-19 11:39:56 +00:00
|
|
|
/** @var SlotRoleRegistry */
|
|
|
|
|
private $slotRoleRegistry;
|
|
|
|
|
|
2020-01-18 20:25:04 +00:00
|
|
|
/** @var IContentHandlerFactory */
|
|
|
|
|
private $contentHandlerFactory;
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
|
|
|
|
* @todo $blobStore should be allowed to be any BlobStore!
|
|
|
|
|
*
|
2018-06-27 12:16:35 +00:00
|
|
|
* @param ILoadBalancer $loadBalancer
|
2017-08-27 15:29:18 +00:00
|
|
|
* @param SqlBlobStore $blobStore
|
2018-08-02 15:49:27 +00:00
|
|
|
* @param WANObjectCache $cache A cache for caching revision rows. This can be the local
|
2019-06-27 01:33:18 +00:00
|
|
|
* wiki's default instance even if $dbDomain refers to a different wiki, since
|
2018-08-02 15:49:27 +00:00
|
|
|
* makeGlobalKey() is used to constructed a key that allows cached revision rows from
|
|
|
|
|
* the same database to be re-used between wikis. For example, enwiki and frwiki will
|
|
|
|
|
* use the same cache keys for revision rows from the wikidatawiki database, regardless
|
|
|
|
|
* of the cache's default key space.
|
2018-01-29 14:25:49 +00:00
|
|
|
* @param CommentStore $commentStore
|
2018-01-29 15:54:02 +00:00
|
|
|
* @param NameTableStore $contentModelStore
|
|
|
|
|
* @param NameTableStore $slotRoleStore
|
2018-11-19 11:39:56 +00:00
|
|
|
* @param SlotRoleRegistry $slotRoleRegistry
|
2017-09-12 17:12:29 +00:00
|
|
|
* @param ActorMigration $actorMigration
|
2020-01-18 20:25:04 +00:00
|
|
|
* @param IContentHandlerFactory $contentHandlerFactory
|
2019-06-27 01:33:18 +00:00
|
|
|
* @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one
|
2017-08-27 15:29:18 +00:00
|
|
|
*/
|
|
|
|
|
public function __construct(
|
2018-06-27 12:16:35 +00:00
|
|
|
ILoadBalancer $loadBalancer,
|
2017-08-27 15:29:18 +00:00
|
|
|
SqlBlobStore $blobStore,
|
|
|
|
|
WANObjectCache $cache,
|
2018-01-29 14:25:49 +00:00
|
|
|
CommentStore $commentStore,
|
2018-01-29 15:54:02 +00:00
|
|
|
NameTableStore $contentModelStore,
|
|
|
|
|
NameTableStore $slotRoleStore,
|
2018-11-19 11:39:56 +00:00
|
|
|
SlotRoleRegistry $slotRoleRegistry,
|
2017-09-12 17:12:29 +00:00
|
|
|
ActorMigration $actorMigration,
|
2020-01-18 20:25:04 +00:00
|
|
|
IContentHandlerFactory $contentHandlerFactory,
|
2019-06-27 01:33:18 +00:00
|
|
|
$dbDomain = false
|
2017-08-27 15:29:18 +00:00
|
|
|
) {
|
2019-06-27 01:33:18 +00:00
|
|
|
Assert::parameterType( 'string|boolean', $dbDomain, '$dbDomain' );
|
2018-01-29 15:54:02 +00:00
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
$this->loadBalancer = $loadBalancer;
|
|
|
|
|
$this->blobStore = $blobStore;
|
|
|
|
|
$this->cache = $cache;
|
2018-01-29 14:25:49 +00:00
|
|
|
$this->commentStore = $commentStore;
|
2018-01-29 15:54:02 +00:00
|
|
|
$this->contentModelStore = $contentModelStore;
|
|
|
|
|
$this->slotRoleStore = $slotRoleStore;
|
2018-11-19 11:39:56 +00:00
|
|
|
$this->slotRoleRegistry = $slotRoleRegistry;
|
2017-09-12 17:12:29 +00:00
|
|
|
$this->actorMigration = $actorMigration;
|
2019-06-27 01:33:18 +00:00
|
|
|
$this->dbDomain = $dbDomain;
|
2018-01-10 16:05:46 +00:00
|
|
|
$this->logger = new NullLogger();
|
2020-01-18 20:25:04 +00:00
|
|
|
$this->contentHandlerFactory = $contentHandlerFactory;
|
2018-01-10 16:05:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setLogger( LoggerInterface $logger ) {
|
|
|
|
|
$this->logger = $logger;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-22 08:48:42 +00:00
|
|
|
/**
|
|
|
|
|
* @return bool Whether the store is read-only
|
|
|
|
|
*/
|
|
|
|
|
public function isReadOnly() {
|
|
|
|
|
return $this->blobStore->isReadOnly();
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
2018-06-27 12:16:35 +00:00
|
|
|
* @return ILoadBalancer
|
2017-08-27 15:29:18 +00:00
|
|
|
*/
|
|
|
|
|
private function getDBLoadBalancer() {
|
|
|
|
|
return $this->loadBalancer;
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-10 18:54:33 +00:00
|
|
|
/**
|
|
|
|
|
* @param int $queryFlags a bit field composed of READ_XXX flags
|
|
|
|
|
*
|
|
|
|
|
* @return DBConnRef
|
|
|
|
|
*/
|
|
|
|
|
private function getDBConnectionRefForQueryFlags( $queryFlags ) {
|
|
|
|
|
list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
|
|
|
|
|
return $this->getDBConnectionRef( $mode );
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
|
|
|
|
* @param int $mode DB_MASTER or DB_REPLICA
|
|
|
|
|
*
|
2019-08-04 14:50:50 +00:00
|
|
|
* @param array $groups
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return DBConnRef
|
|
|
|
|
*/
|
2019-08-04 14:50:50 +00:00
|
|
|
private function getDBConnectionRef( $mode, $groups = [] ) {
|
2017-08-27 15:29:18 +00:00
|
|
|
$lb = $this->getDBLoadBalancer();
|
2019-08-04 14:50:50 +00:00
|
|
|
return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines the page Title based on the available information.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this corresponds to Revision::getTitle
|
|
|
|
|
*
|
2018-01-11 15:43:18 +00:00
|
|
|
* @note this method should be private, external use should be avoided!
|
|
|
|
|
*
|
2017-08-27 15:29:18 +00:00
|
|
|
* @param int|null $pageId
|
|
|
|
|
* @param int|null $revId
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
*
|
|
|
|
|
* @return Title
|
|
|
|
|
* @throws RevisionAccessException
|
|
|
|
|
*/
|
2018-01-10 16:05:46 +00:00
|
|
|
public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
|
2017-08-27 15:29:18 +00:00
|
|
|
if ( !$pageId && !$revId ) {
|
|
|
|
|
throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-10 16:05:46 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-27 01:33:18 +00:00
|
|
|
$canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
|
2018-01-10 16:05:46 +00:00
|
|
|
list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
// Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
|
2018-01-10 16:05:46 +00:00
|
|
|
if ( $canUseTitleNewFromId ) {
|
2019-07-04 21:24:34 +00:00
|
|
|
$titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
|
2017-08-27 15:29:18 +00:00
|
|
|
// TODO: better foreign title handling (introduce TitleFactory)
|
2018-01-10 16:17:40 +00:00
|
|
|
$title = Title::newFromID( $pageId, $titleFlags );
|
2018-01-10 16:05:46 +00:00
|
|
|
if ( $title ) {
|
|
|
|
|
return $title;
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
|
2018-01-10 16:05:46 +00:00
|
|
|
$canUseRevId = ( $revId !== null && $revId > 0 );
|
|
|
|
|
|
|
|
|
|
if ( $canUseRevId ) {
|
2018-01-20 00:44:30 +00:00
|
|
|
$dbr = $this->getDBConnectionRef( $dbMode );
|
2017-08-27 15:29:18 +00:00
|
|
|
// @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
|
|
|
|
|
$row = $dbr->selectRow(
|
|
|
|
|
[ 'revision', 'page' ],
|
|
|
|
|
[
|
|
|
|
|
'page_namespace',
|
|
|
|
|
'page_title',
|
|
|
|
|
'page_id',
|
|
|
|
|
'page_latest',
|
|
|
|
|
'page_is_redirect',
|
|
|
|
|
'page_len',
|
|
|
|
|
],
|
|
|
|
|
[ 'rev_id' => $revId ],
|
|
|
|
|
__METHOD__,
|
|
|
|
|
$dbOptions,
|
|
|
|
|
[ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
|
|
|
|
|
);
|
|
|
|
|
if ( $row ) {
|
|
|
|
|
// TODO: better foreign title handling (introduce TitleFactory)
|
2018-01-10 16:05:46 +00:00
|
|
|
return Title::newFromRow( $row );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-10 16:05:46 +00:00
|
|
|
// 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.',
|
2018-02-15 14:15:45 +00:00
|
|
|
[ 'trace' => wfBacktrace() ]
|
2018-01-10 16:05:46 +00:00
|
|
|
);
|
|
|
|
|
return $title;
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-10 16:05:46 +00:00
|
|
|
throw new RevisionAccessException(
|
|
|
|
|
"Could not determine title for page ID $pageId and revision ID $revId"
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param mixed $value
|
|
|
|
|
* @param string $name
|
|
|
|
|
*
|
2018-04-27 19:53:19 +00:00
|
|
|
* @throws IncompleteRevisionException if $value is null
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return mixed $value, if $value is not null
|
|
|
|
|
*/
|
|
|
|
|
private function failOnNull( $value, $name ) {
|
|
|
|
|
if ( $value === null ) {
|
|
|
|
|
throw new IncompleteRevisionException(
|
|
|
|
|
"$name must not be " . var_export( $value, true ) . "!"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param mixed $value
|
|
|
|
|
* @param string $name
|
|
|
|
|
*
|
2018-04-27 19:53:19 +00:00
|
|
|
* @throws IncompleteRevisionException if $value is empty
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return mixed $value, if $value is not null
|
|
|
|
|
*/
|
|
|
|
|
private function failOnEmpty( $value, $name ) {
|
|
|
|
|
if ( $value === null || $value === 0 || $value === '' ) {
|
|
|
|
|
throw new IncompleteRevisionException(
|
|
|
|
|
"$name must not be " . var_export( $value, true ) . "!"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-01-29 14:34:05 +00:00
|
|
|
* Insert a new revision into the database, returning the new revision record
|
|
|
|
|
* on success and dies horribly on failure.
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::insertOn
|
|
|
|
|
*
|
|
|
|
|
* @param RevisionRecord $rev
|
|
|
|
|
* @param IDatabase $dbw (master connection)
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionRecord the new revision record.
|
|
|
|
|
*/
|
|
|
|
|
public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
|
|
|
|
|
// TODO: pass in a DBTransactionContext instead of a database connection.
|
2019-07-04 07:46:39 +00:00
|
|
|
$this->checkDatabaseDomain( $dbw );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
$slotRoles = $rev->getSlotRoles();
|
|
|
|
|
|
|
|
|
|
// Make sure the main slot is always provided throughout migration
|
2018-09-24 21:10:08 +00:00
|
|
|
if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
|
2019-12-17 16:20:32 +00:00
|
|
|
throw new IncompleteRevisionException(
|
2018-04-17 07:49:20 +00:00
|
|
|
'main slot must be provided'
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
// Checks
|
|
|
|
|
$this->failOnNull( $rev->getSize(), 'size field' );
|
|
|
|
|
$this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
|
|
|
|
|
$this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
|
|
|
|
|
$comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
|
|
|
|
|
$user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
|
|
|
|
|
$this->failOnNull( $user->getId(), 'user field' );
|
|
|
|
|
$this->failOnEmpty( $user->getName(), 'user_text field' );
|
|
|
|
|
|
2018-09-10 19:34:31 +00:00
|
|
|
if ( !$rev->isReadyForInsertion() ) {
|
|
|
|
|
// This is here for future-proofing. At the time this check being added, it
|
|
|
|
|
// was redundant to the individual checks above.
|
|
|
|
|
throw new IncompleteRevisionException( 'Revision is incomplete' );
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-06 13:20:16 +00:00
|
|
|
if ( $slotRoles == [ SlotRecord::MAIN ] ) {
|
|
|
|
|
// T239717: If the main slot is the only slot, make sure the revision's nominal size
|
|
|
|
|
// and hash match the main slot's nominal size and hash.
|
|
|
|
|
$mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
|
|
|
|
|
Assert::precondition(
|
|
|
|
|
$mainSlot->getSize() === $rev->getSize(),
|
|
|
|
|
'The revisions\'s size must match the main slot\'s size (see T239717)'
|
|
|
|
|
);
|
|
|
|
|
Assert::precondition(
|
|
|
|
|
$mainSlot->getSha1() === $rev->getSha1(),
|
|
|
|
|
'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
// TODO: we shouldn't need an actual Title here.
|
|
|
|
|
$title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
|
|
|
|
|
$pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
|
|
|
|
|
|
|
|
|
|
$parentId = $rev->getParentId() === null
|
|
|
|
|
? $this->getPreviousRevisionId( $dbw, $rev )
|
|
|
|
|
: $rev->getParentId();
|
|
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
/** @var RevisionRecord $rev */
|
|
|
|
|
$rev = $dbw->doAtomicSection(
|
|
|
|
|
__METHOD__,
|
|
|
|
|
function ( IDatabase $dbw, $fname ) use (
|
|
|
|
|
$rev,
|
|
|
|
|
$user,
|
|
|
|
|
$comment,
|
|
|
|
|
$title,
|
|
|
|
|
$pageId,
|
|
|
|
|
$parentId
|
|
|
|
|
) {
|
|
|
|
|
return $this->insertRevisionInternal(
|
|
|
|
|
$rev,
|
|
|
|
|
$dbw,
|
|
|
|
|
$user,
|
|
|
|
|
$comment,
|
|
|
|
|
$title,
|
|
|
|
|
$pageId,
|
|
|
|
|
$parentId
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
// sanity checks
|
|
|
|
|
Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
|
|
|
|
|
Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
|
|
|
|
|
Assert::postcondition(
|
|
|
|
|
$rev->getComment( RevisionRecord::RAW ) !== null,
|
|
|
|
|
'revision must have a comment'
|
|
|
|
|
);
|
|
|
|
|
Assert::postcondition(
|
|
|
|
|
$rev->getUser( RevisionRecord::RAW ) !== null,
|
|
|
|
|
'revision must have a user'
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
// Trigger exception if the main slot is missing.
|
2018-06-26 17:26:33 +00:00
|
|
|
// Technically, this could go away after MCR migration: while
|
2018-04-17 07:49:20 +00:00
|
|
|
// calling code may require a main slot to exist, RevisionStore
|
|
|
|
|
// really should not know or care about that requirement.
|
2018-09-24 21:10:08 +00:00
|
|
|
$rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
|
2018-04-17 07:49:20 +00:00
|
|
|
|
|
|
|
|
foreach ( $slotRoles as $role ) {
|
|
|
|
|
$slot = $rev->getSlot( $role, RevisionRecord::RAW );
|
|
|
|
|
Assert::postcondition(
|
|
|
|
|
$slot->getContent() !== null,
|
2018-09-07 17:01:32 +00:00
|
|
|
$role . ' slot must have content'
|
2018-04-17 07:49:20 +00:00
|
|
|
);
|
|
|
|
|
Assert::postcondition(
|
|
|
|
|
$slot->hasRevision(),
|
2018-09-07 17:01:32 +00:00
|
|
|
$role . ' slot must have a revision associated'
|
2018-04-17 07:49:20 +00:00
|
|
|
);
|
|
|
|
|
}
|
2018-01-29 15:54:02 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
Hooks::run( 'RevisionRecordInserted', [ $rev ] );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2020-03-31 16:15:04 +00:00
|
|
|
// Soft deprecated in 1.31, hard deprecated in 1.35
|
2018-04-17 07:49:20 +00:00
|
|
|
$legacyRevision = new Revision( $rev );
|
2020-03-31 16:15:04 +00:00
|
|
|
Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ], '1.31' );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
return $rev;
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
private function insertRevisionInternal(
|
|
|
|
|
RevisionRecord $rev,
|
|
|
|
|
IDatabase $dbw,
|
|
|
|
|
User $user,
|
|
|
|
|
CommentStoreComment $comment,
|
|
|
|
|
Title $title,
|
|
|
|
|
$pageId,
|
|
|
|
|
$parentId
|
|
|
|
|
) {
|
|
|
|
|
$slotRoles = $rev->getSlotRoles();
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
$revisionRow = $this->insertRevisionRowOn(
|
|
|
|
|
$dbw,
|
|
|
|
|
$rev,
|
|
|
|
|
$title,
|
|
|
|
|
$parentId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$revisionId = $revisionRow['rev_id'];
|
|
|
|
|
|
|
|
|
|
$blobHints = [
|
|
|
|
|
BlobStore::PAGE_HINT => $pageId,
|
|
|
|
|
BlobStore::REVISION_HINT => $revisionId,
|
|
|
|
|
BlobStore::PARENT_HINT => $parentId,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$newSlots = [];
|
|
|
|
|
foreach ( $slotRoles as $role ) {
|
|
|
|
|
$slot = $rev->getSlot( $role, RevisionRecord::RAW );
|
|
|
|
|
|
Fix undeletion write-both/read-old mode.
With the new schema, undeletion does not create now slot rows.
However, when in read-old mode, we may still have un-migrated
archive rows. When undeletion based on such a row, we do need
to insert a slot row.
This also changes the return value of SlotRecord for
SCHEMA_COMPAT_READ_OLD mode from null to the negative value of
rev_text_id, for consistency. Conceptually, the emulated slots
in SCHEMA_COMPAT_OLD now have a "virtual" content ID, hich is
however distinct from any real content ID that may exist in
the future. Such virtual content IDs are however not assigned
in SCHEMA_COMPAT_WRITE_BOTH mode. In that mode, unmigrated
rows can be detected by calling hasContentId() on the main
slot. Migrated rows will return true here and provide the
id of the associated content row, even in
SCHEMA_COMPAT_READ_OLD mode. This is particularly essential
for undeletion, which needs to maintain the association
between revision and slot rows even in SCHEMA_COMPAT_READ_OLD
mode.
Bug: T174024
Bug: T194015
Bug: T183488
Change-Id: I88ee9809b9752e1e72d635d62e82008315402901
2018-08-27 19:45:40 +00:00
|
|
|
// If the SlotRecord already has a revision ID set, this means it already exists
|
|
|
|
|
// in the database, and should already belong to the current revision.
|
|
|
|
|
// However, a slot may already have a revision, but no content ID, if the slot
|
|
|
|
|
// is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
|
|
|
|
|
// mode, and the respective archive row was not yet migrated to the new schema.
|
|
|
|
|
// In that case, a new slot row (and content row) must be inserted even during
|
|
|
|
|
// undeletion.
|
|
|
|
|
if ( $slot->hasRevision() && $slot->hasContentId() ) {
|
2018-04-17 07:49:20 +00:00
|
|
|
// TODO: properly abort transaction if the assertion fails!
|
|
|
|
|
Assert::parameter(
|
|
|
|
|
$slot->getRevision() === $revisionId,
|
|
|
|
|
'slot role ' . $slot->getRole(),
|
|
|
|
|
'Existing slot should belong to revision '
|
|
|
|
|
. $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Slot exists, nothing to do, move along.
|
|
|
|
|
// This happens when restoring archived revisions.
|
|
|
|
|
|
|
|
|
|
$newSlots[$role] = $slot;
|
|
|
|
|
} else {
|
|
|
|
|
$newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
$this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
|
|
|
|
|
|
|
|
|
|
$rev = new RevisionStoreRecord(
|
|
|
|
|
$title,
|
|
|
|
|
$user,
|
|
|
|
|
$comment,
|
|
|
|
|
(object)$revisionRow,
|
|
|
|
|
new RevisionSlots( $newSlots ),
|
2019-06-27 01:33:18 +00:00
|
|
|
$this->dbDomain
|
2018-04-17 07:49:20 +00:00
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
return $rev;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param IDatabase $dbw
|
|
|
|
|
* @param int $revisionId
|
|
|
|
|
* @param SlotRecord $protoSlot
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @param array $blobHints See the BlobStore::XXX_HINT constants
|
|
|
|
|
* @return SlotRecord
|
|
|
|
|
*/
|
|
|
|
|
private function insertSlotOn(
|
|
|
|
|
IDatabase $dbw,
|
|
|
|
|
$revisionId,
|
|
|
|
|
SlotRecord $protoSlot,
|
|
|
|
|
Title $title,
|
|
|
|
|
array $blobHints = []
|
|
|
|
|
) {
|
|
|
|
|
if ( $protoSlot->hasAddress() ) {
|
|
|
|
|
$blobAddress = $protoSlot->getAddress();
|
2018-01-29 15:54:02 +00:00
|
|
|
} else {
|
2018-04-17 07:49:20 +00:00
|
|
|
$blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
|
2018-01-29 15:54:02 +00:00
|
|
|
}
|
|
|
|
|
|
Fix undeletion write-both/read-old mode.
With the new schema, undeletion does not create now slot rows.
However, when in read-old mode, we may still have un-migrated
archive rows. When undeletion based on such a row, we do need
to insert a slot row.
This also changes the return value of SlotRecord for
SCHEMA_COMPAT_READ_OLD mode from null to the negative value of
rev_text_id, for consistency. Conceptually, the emulated slots
in SCHEMA_COMPAT_OLD now have a "virtual" content ID, hich is
however distinct from any real content ID that may exist in
the future. Such virtual content IDs are however not assigned
in SCHEMA_COMPAT_WRITE_BOTH mode. In that mode, unmigrated
rows can be detected by calling hasContentId() on the main
slot. Migrated rows will return true here and provide the
id of the associated content row, even in
SCHEMA_COMPAT_READ_OLD mode. This is particularly essential
for undeletion, which needs to maintain the association
between revision and slot rows even in SCHEMA_COMPAT_READ_OLD
mode.
Bug: T174024
Bug: T194015
Bug: T183488
Change-Id: I88ee9809b9752e1e72d635d62e82008315402901
2018-08-27 19:45:40 +00:00
|
|
|
$contentId = null;
|
|
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
if ( $protoSlot->hasContentId() ) {
|
|
|
|
|
$contentId = $protoSlot->getContentId();
|
|
|
|
|
} else {
|
|
|
|
|
$contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
|
2018-04-17 07:49:20 +00:00
|
|
|
}
|
2018-01-29 15:54:02 +00:00
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
$this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
$savedSlot = SlotRecord::newSaved(
|
|
|
|
|
$revisionId,
|
|
|
|
|
$contentId,
|
|
|
|
|
$blobAddress,
|
|
|
|
|
$protoSlot
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
return $savedSlot;
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
/**
|
|
|
|
|
* Insert IP revision into ip_changes for use when querying for a range.
|
|
|
|
|
* @param IDatabase $dbw
|
|
|
|
|
* @param User $user
|
|
|
|
|
* @param RevisionRecord $rev
|
|
|
|
|
* @param int $revisionId
|
|
|
|
|
*/
|
|
|
|
|
private function insertIpChangesRow(
|
|
|
|
|
IDatabase $dbw,
|
|
|
|
|
User $user,
|
|
|
|
|
RevisionRecord $rev,
|
|
|
|
|
$revisionId
|
|
|
|
|
) {
|
2019-06-25 18:53:15 +00:00
|
|
|
if ( $user->getId() === 0 && IPUtils::isValid( $user->getName() ) ) {
|
2017-08-27 15:29:18 +00:00
|
|
|
$ipcRow = [
|
2018-01-29 15:54:02 +00:00
|
|
|
'ipc_rev_id' => $revisionId,
|
2018-04-17 07:49:20 +00:00
|
|
|
'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
|
2019-06-25 18:53:15 +00:00
|
|
|
'ipc_hex' => IPUtils::toHex( $user->getName() ),
|
2017-08-27 15:29:18 +00:00
|
|
|
];
|
|
|
|
|
$dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
|
|
|
|
|
}
|
2018-04-17 07:49:20 +00:00
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
/**
|
|
|
|
|
* @param IDatabase $dbw
|
|
|
|
|
* @param RevisionRecord $rev
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @param int $parentId
|
|
|
|
|
*
|
|
|
|
|
* @return array a revision table row
|
|
|
|
|
*
|
|
|
|
|
* @throws MWException
|
|
|
|
|
* @throws MWUnknownContentModelException
|
|
|
|
|
*/
|
|
|
|
|
private function insertRevisionRowOn(
|
|
|
|
|
IDatabase $dbw,
|
|
|
|
|
RevisionRecord $rev,
|
|
|
|
|
Title $title,
|
|
|
|
|
$parentId
|
|
|
|
|
) {
|
|
|
|
|
$revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
|
2018-01-29 15:54:02 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
list( $commentFields, $commentCallback ) =
|
|
|
|
|
$this->commentStore->insertWithTempTable(
|
|
|
|
|
$dbw,
|
|
|
|
|
'rev_comment',
|
|
|
|
|
$rev->getComment( RevisionRecord::RAW )
|
|
|
|
|
);
|
|
|
|
|
$revisionRow += $commentFields;
|
2018-01-29 15:54:02 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
list( $actorFields, $actorCallback ) =
|
|
|
|
|
$this->actorMigration->getInsertValuesWithTempTable(
|
|
|
|
|
$dbw,
|
|
|
|
|
'rev_user',
|
|
|
|
|
$rev->getUser( RevisionRecord::RAW )
|
|
|
|
|
);
|
|
|
|
|
$revisionRow += $actorFields;
|
2018-01-29 15:54:02 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
$dbw->insert( 'revision', $revisionRow, __METHOD__ );
|
|
|
|
|
|
|
|
|
|
if ( !isset( $revisionRow['rev_id'] ) ) {
|
|
|
|
|
// only if auto-increment was used
|
|
|
|
|
$revisionRow['rev_id'] = intval( $dbw->insertId() );
|
2018-08-22 15:59:56 +00:00
|
|
|
|
|
|
|
|
if ( $dbw->getType() === 'mysql' ) {
|
|
|
|
|
// (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the
|
|
|
|
|
// auto-increment value to disk, so on server restart it might reuse IDs from deleted
|
|
|
|
|
// revisions. We can fix that with an insert with an explicit rev_id value, if necessary.
|
|
|
|
|
|
|
|
|
|
$maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) );
|
|
|
|
|
$table = 'archive';
|
2019-12-17 16:20:32 +00:00
|
|
|
$maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
|
|
|
|
|
if ( $maxRevId2 >= $maxRevId ) {
|
|
|
|
|
$maxRevId = $maxRevId2;
|
|
|
|
|
$table = 'slots';
|
2018-08-22 15:59:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $maxRevId >= $revisionRow['rev_id'] ) {
|
|
|
|
|
$this->logger->debug(
|
|
|
|
|
'__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
|
|
|
|
|
. ' Trying to fix it.',
|
|
|
|
|
[
|
|
|
|
|
'revid' => $revisionRow['rev_id'],
|
|
|
|
|
'table' => $table,
|
|
|
|
|
'maxrevid' => $maxRevId,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) {
|
|
|
|
|
throw new MWException( 'Failed to get database lock for T202032' );
|
|
|
|
|
}
|
|
|
|
|
$fname = __METHOD__;
|
2019-06-27 01:33:18 +00:00
|
|
|
$dbw->onTransactionResolution(
|
|
|
|
|
function ( $trigger, IDatabase $dbw ) use ( $fname ) {
|
|
|
|
|
$dbw->unlock( 'fix-for-T202032', $fname );
|
|
|
|
|
}
|
|
|
|
|
);
|
2018-08-22 15:59:56 +00:00
|
|
|
|
|
|
|
|
$dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ );
|
|
|
|
|
|
|
|
|
|
// The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction
|
|
|
|
|
// isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing
|
|
|
|
|
// inserts too, though, at least on MariaDB 10.1.29.
|
|
|
|
|
//
|
|
|
|
|
// Don't try to lock `revision` in this way, it'll deadlock if there are concurrent
|
|
|
|
|
// transactions in this code path thanks to the row lock from the original ->insert() above.
|
|
|
|
|
//
|
|
|
|
|
// And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning
|
|
|
|
|
// that's for non-MySQL DBs.
|
|
|
|
|
$row1 = $dbw->query(
|
2019-03-29 23:20:50 +00:00
|
|
|
$dbw->selectSQLText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
|
2018-08-22 15:59:56 +00:00
|
|
|
)->fetchObject();
|
2019-12-17 16:20:32 +00:00
|
|
|
|
|
|
|
|
$row2 = $dbw->query(
|
|
|
|
|
$dbw->selectSQLText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
|
|
|
|
|
. ' FOR UPDATE'
|
|
|
|
|
)->fetchObject();
|
|
|
|
|
|
2018-08-22 15:59:56 +00:00
|
|
|
$maxRevId = max(
|
|
|
|
|
$maxRevId,
|
|
|
|
|
$row1 ? intval( $row1->v ) : 0,
|
|
|
|
|
$row2 ? intval( $row2->v ) : 0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent
|
|
|
|
|
// transactions will throw a duplicate key error here. It doesn't seem worth trying
|
|
|
|
|
// to avoid that.
|
|
|
|
|
$revisionRow['rev_id'] = $maxRevId + 1;
|
|
|
|
|
$dbw->insert( 'revision', $revisionRow, __METHOD__ );
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-29 15:54:02 +00:00
|
|
|
}
|
|
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
$commentCallback( $revisionRow['rev_id'] );
|
|
|
|
|
$actorCallback( $revisionRow['rev_id'], $revisionRow );
|
2018-01-29 15:54:02 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
return $revisionRow;
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
/**
|
|
|
|
|
* @param IDatabase $dbw
|
|
|
|
|
* @param RevisionRecord $rev
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @param int $parentId
|
|
|
|
|
*
|
|
|
|
|
* @return array [ 0 => array $revisionRow, 1 => callable ]
|
|
|
|
|
* @throws MWException
|
|
|
|
|
* @throws MWUnknownContentModelException
|
|
|
|
|
*/
|
|
|
|
|
private function getBaseRevisionRow(
|
|
|
|
|
IDatabase $dbw,
|
|
|
|
|
RevisionRecord $rev,
|
|
|
|
|
Title $title,
|
|
|
|
|
$parentId
|
|
|
|
|
) {
|
|
|
|
|
// Record the edit in revisions
|
|
|
|
|
$revisionRow = [
|
|
|
|
|
'rev_page' => $rev->getPageId(),
|
|
|
|
|
'rev_parent_id' => $parentId,
|
|
|
|
|
'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
|
|
|
|
|
'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
|
|
|
|
|
'rev_deleted' => $rev->getVisibility(),
|
|
|
|
|
'rev_len' => $rev->getSize(),
|
|
|
|
|
'rev_sha1' => $rev->getSha1(),
|
|
|
|
|
];
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
if ( $rev->getId() !== null ) {
|
|
|
|
|
// Needed to restore revisions with their original ID
|
|
|
|
|
$revisionRow['rev_id'] = $rev->getId();
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
return $revisionRow;
|
|
|
|
|
}
|
2018-06-13 18:18:47 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
/**
|
|
|
|
|
* @param SlotRecord $slot
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @param array $blobHints See the BlobStore::XXX_HINT constants
|
|
|
|
|
*
|
|
|
|
|
* @throws MWException
|
|
|
|
|
* @return string the blob address
|
|
|
|
|
*/
|
|
|
|
|
private function storeContentBlob(
|
|
|
|
|
SlotRecord $slot,
|
|
|
|
|
Title $title,
|
|
|
|
|
array $blobHints = []
|
|
|
|
|
) {
|
|
|
|
|
$content = $slot->getContent();
|
|
|
|
|
$format = $content->getDefaultFormat();
|
|
|
|
|
$model = $content->getModel();
|
|
|
|
|
|
2018-11-19 11:39:56 +00:00
|
|
|
$this->checkContent( $content, $title, $slot->getRole() );
|
2018-04-17 07:49:20 +00:00
|
|
|
|
|
|
|
|
return $this->blobStore->storeBlob(
|
|
|
|
|
$content->serialize( $format ),
|
|
|
|
|
// These hints "leak" some information from the higher abstraction layer to
|
|
|
|
|
// low level storage to allow for optimization.
|
|
|
|
|
array_merge(
|
|
|
|
|
$blobHints,
|
|
|
|
|
[
|
|
|
|
|
BlobStore::DESIGNATION_HINT => 'page-content',
|
|
|
|
|
BlobStore::ROLE_HINT => $slot->getRole(),
|
|
|
|
|
BlobStore::SHA1_HINT => $slot->getSha1(),
|
|
|
|
|
BlobStore::MODEL_HINT => $model,
|
|
|
|
|
BlobStore::FORMAT_HINT => $format,
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-29 15:54:02 +00:00
|
|
|
/**
|
|
|
|
|
* @param SlotRecord $slot
|
|
|
|
|
* @param IDatabase $dbw
|
|
|
|
|
* @param int $revisionId
|
|
|
|
|
* @param int $contentId
|
|
|
|
|
*/
|
|
|
|
|
private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
|
|
|
|
|
$slotRow = [
|
|
|
|
|
'slot_revision_id' => $revisionId,
|
|
|
|
|
'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
|
|
|
|
|
'slot_content_id' => $contentId,
|
|
|
|
|
// If the slot has a specific origin use that ID, otherwise use the ID of the revision
|
|
|
|
|
// that we just inserted.
|
|
|
|
|
'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
|
|
|
|
|
];
|
|
|
|
|
$dbw->insert( 'slots', $slotRow, __METHOD__ );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param SlotRecord $slot
|
|
|
|
|
* @param IDatabase $dbw
|
|
|
|
|
* @param string $blobAddress
|
|
|
|
|
* @return int content row ID
|
|
|
|
|
*/
|
|
|
|
|
private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
|
|
|
|
|
$contentRow = [
|
|
|
|
|
'content_size' => $slot->getSize(),
|
|
|
|
|
'content_sha1' => $slot->getSha1(),
|
|
|
|
|
'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
|
|
|
|
|
'content_address' => $blobAddress,
|
|
|
|
|
];
|
|
|
|
|
$dbw->insert( 'content', $contentRow, __METHOD__ );
|
|
|
|
|
return intval( $dbw->insertId() );
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
|
|
|
|
* MCR migration note: this corresponds to Revision::checkContentModel
|
|
|
|
|
*
|
|
|
|
|
* @param Content $content
|
|
|
|
|
* @param Title $title
|
2018-11-19 11:39:56 +00:00
|
|
|
* @param string $role
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @throws MWException
|
|
|
|
|
* @throws MWUnknownContentModelException
|
|
|
|
|
*/
|
2018-11-19 11:39:56 +00:00
|
|
|
private function checkContent( Content $content, Title $title, $role ) {
|
2017-08-27 15:29:18 +00:00
|
|
|
// Note: may return null for revisions that have not yet been inserted
|
|
|
|
|
|
|
|
|
|
$model = $content->getModel();
|
|
|
|
|
$format = $content->getDefaultFormat();
|
|
|
|
|
$handler = $content->getContentHandler();
|
|
|
|
|
|
|
|
|
|
$name = "$title";
|
|
|
|
|
|
|
|
|
|
if ( !$handler->isSupportedFormat( $format ) ) {
|
|
|
|
|
throw new MWException( "Can't use format $format with content model $model on $name" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$content->isValid() ) {
|
|
|
|
|
throw new MWException(
|
|
|
|
|
"New content for $name is not valid! Content model is $model"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new null-revision for insertion into a page's
|
|
|
|
|
* history. This will not re-save the text, but simply refer
|
|
|
|
|
* to the text from the previous version.
|
|
|
|
|
*
|
|
|
|
|
* Such revisions can for instance identify page rename
|
|
|
|
|
* operations and other such meta-modifications.
|
|
|
|
|
*
|
2018-07-26 16:31:49 +00:00
|
|
|
* @note This method grabs a FOR UPDATE lock on the relevant row of the page table,
|
2018-05-09 10:06:51 +00:00
|
|
|
* to prevent a new revision from being inserted before the null revision has been written
|
|
|
|
|
* to the database.
|
|
|
|
|
*
|
2017-08-27 15:29:18 +00:00
|
|
|
* MCR migration note: this replaces Revision::newNullRevision
|
|
|
|
|
*
|
|
|
|
|
* @todo Introduce newFromParentRevision(). newNullRevision can then be based on that
|
|
|
|
|
* (or go away).
|
|
|
|
|
*
|
2018-05-09 10:06:51 +00:00
|
|
|
* @param IDatabase $dbw used for obtaining the lock on the page table row
|
2017-08-27 15:29:18 +00:00
|
|
|
* @param Title $title Title of the page to read from
|
|
|
|
|
* @param CommentStoreComment $comment RevisionRecord's summary
|
|
|
|
|
* @param bool $minor Whether the revision should be considered as minor
|
|
|
|
|
* @param User $user The user to attribute the revision to
|
2018-05-09 10:06:51 +00:00
|
|
|
*
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return RevisionRecord|null RevisionRecord or null on error
|
|
|
|
|
*/
|
|
|
|
|
public function newNullRevision(
|
|
|
|
|
IDatabase $dbw,
|
|
|
|
|
Title $title,
|
|
|
|
|
CommentStoreComment $comment,
|
|
|
|
|
$minor,
|
|
|
|
|
User $user
|
|
|
|
|
) {
|
2019-07-04 07:46:39 +00:00
|
|
|
$this->checkDatabaseDomain( $dbw );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-10-04 10:49:50 +00:00
|
|
|
$pageId = $title->getArticleID();
|
|
|
|
|
|
2018-05-09 10:06:51 +00:00
|
|
|
// T51581: Lock the page table row to ensure no other process
|
|
|
|
|
// is adding a revision to the page at the same time.
|
|
|
|
|
// Avoid locking extra tables, compare T191892.
|
|
|
|
|
$pageLatest = $dbw->selectField(
|
|
|
|
|
'page',
|
|
|
|
|
'page_latest',
|
2018-10-04 10:49:50 +00:00
|
|
|
[ 'page_id' => $pageId ],
|
2017-08-27 15:29:18 +00:00
|
|
|
__METHOD__,
|
2018-05-09 10:06:51 +00:00
|
|
|
[ 'FOR UPDATE' ]
|
2017-08-27 15:29:18 +00:00
|
|
|
);
|
|
|
|
|
|
2018-05-09 10:06:51 +00:00
|
|
|
if ( !$pageLatest ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-07-10 17:54:11 +00:00
|
|
|
// Fetch the actual revision row from master, without locking all extra tables.
|
|
|
|
|
$oldRevision = $this->loadRevisionFromConds(
|
|
|
|
|
$dbw,
|
|
|
|
|
[ 'rev_id' => intval( $pageLatest ) ],
|
|
|
|
|
self::READ_LATEST,
|
|
|
|
|
$title
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-10-04 10:49:50 +00:00
|
|
|
if ( !$oldRevision ) {
|
|
|
|
|
$msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
|
|
|
|
|
$this->logger->error(
|
|
|
|
|
$msg,
|
|
|
|
|
[ 'exception' => new RuntimeException( $msg ) ]
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-09 10:06:51 +00:00
|
|
|
// Construct the new revision
|
2020-04-02 20:00:24 +00:00
|
|
|
$timestamp = MWTimestamp::now( TS_MW );
|
2018-05-09 10:06:51 +00:00
|
|
|
$newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-05-09 10:06:51 +00:00
|
|
|
$newRevision->setComment( $comment );
|
|
|
|
|
$newRevision->setUser( $user );
|
|
|
|
|
$newRevision->setTimestamp( $timestamp );
|
|
|
|
|
$newRevision->setMinorEdit( $minor );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-05-09 10:06:51 +00:00
|
|
|
return $newRevision;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* MCR migration note: this replaces Revision::isUnpatrolled
|
|
|
|
|
*
|
2017-12-27 15:46:03 +00:00
|
|
|
* @todo This is overly specific, so move or kill this method.
|
|
|
|
|
*
|
2018-01-07 10:38:43 +00:00
|
|
|
* @param RevisionRecord $rev
|
2017-12-27 15:46:03 +00:00
|
|
|
*
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return int Rcid of the unpatrolled row, zero if there isn't one
|
|
|
|
|
*/
|
2017-12-27 15:46:03 +00:00
|
|
|
public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
|
2017-08-27 15:29:18 +00:00
|
|
|
$rc = $this->getRecentChange( $rev );
|
2018-04-13 21:36:34 +00:00
|
|
|
if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
|
2017-08-27 15:29:18 +00:00
|
|
|
return $rc->getAttribute( 'rc_id' );
|
|
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the RC object belonging to the current revision, if there's one
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::getRecentChange
|
|
|
|
|
*
|
|
|
|
|
* @todo move this somewhere else?
|
|
|
|
|
*
|
|
|
|
|
* @param RevisionRecord $rev
|
|
|
|
|
* @param int $flags (optional) $flags include:
|
|
|
|
|
* IDBAccessObject::READ_LATEST: Select the data from the master
|
|
|
|
|
*
|
|
|
|
|
* @return null|RecentChange
|
|
|
|
|
*/
|
|
|
|
|
public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
|
|
|
|
|
list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
|
|
|
|
|
|
|
|
|
|
$rc = RecentChange::newFromConds(
|
2019-12-04 14:41:18 +00:00
|
|
|
[ 'rc_this_oldid' => $rev->getId() ],
|
2017-08-27 15:29:18 +00:00
|
|
|
__METHOD__,
|
|
|
|
|
$dbType
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// XXX: cache this locally? Glue it to the RevisionRecord?
|
|
|
|
|
return $rc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Loads a Content object based on a slot row.
|
|
|
|
|
*
|
|
|
|
|
* This method does not call $slot->getContent(), and may be used as a callback
|
|
|
|
|
* called by $slot->getContent().
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this roughly corresponds to Revision::getContentInternal
|
|
|
|
|
*
|
|
|
|
|
* @param SlotRecord $slot The SlotRecord to load content for
|
|
|
|
|
* @param string|null $blobData The content blob, in the form indicated by $blobFlags
|
2018-01-12 11:52:31 +00:00
|
|
|
* @param string|null $blobFlags Flags indicating how $blobData needs to be processed.
|
2018-01-12 13:51:56 +00:00
|
|
|
* 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.
|
2017-08-27 15:29:18 +00:00
|
|
|
* @param string|null $blobFormat MIME type indicating how $dataBlob is encoded
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
*
|
2018-04-27 19:53:19 +00:00
|
|
|
* @throws RevisionAccessException
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return Content
|
|
|
|
|
*/
|
|
|
|
|
private function loadSlotContent(
|
|
|
|
|
SlotRecord $slot,
|
|
|
|
|
$blobData = null,
|
2018-01-12 11:52:31 +00:00
|
|
|
$blobFlags = null,
|
2017-08-27 15:29:18 +00:00
|
|
|
$blobFormat = null,
|
|
|
|
|
$queryFlags = 0
|
|
|
|
|
) {
|
|
|
|
|
if ( $blobData !== null ) {
|
|
|
|
|
Assert::parameterType( 'string', $blobData, '$blobData' );
|
2018-01-12 11:52:31 +00:00
|
|
|
Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
$cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
|
|
|
|
|
|
2018-01-12 11:52:31 +00:00
|
|
|
if ( $blobFlags === null ) {
|
2018-01-12 13:51:56 +00:00
|
|
|
// No blob flags, so use the blob verbatim.
|
2018-01-12 11:52:31 +00:00
|
|
|
$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)"
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
2018-01-12 11:52:31 +00:00
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
} else {
|
|
|
|
|
$address = $slot->getAddress();
|
|
|
|
|
try {
|
|
|
|
|
$data = $this->blobStore->getBlob( $address, $queryFlags );
|
|
|
|
|
} catch ( BlobAccessException $e ) {
|
|
|
|
|
throw new RevisionAccessException(
|
|
|
|
|
"Failed to load data blob from $address: " . $e->getMessage(), 0, $e
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-18 20:25:04 +00:00
|
|
|
return $this->contentHandlerFactory
|
|
|
|
|
->getContentHandler( $slot->getModel() )
|
|
|
|
|
->unserializeContent( $data, $blobFormat );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load a page revision from a given revision ID number.
|
|
|
|
|
* Returns null if no such revision can be found.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::newFromId
|
|
|
|
|
*
|
|
|
|
|
* $flags include:
|
|
|
|
|
* IDBAccessObject::READ_LATEST: Select the data from the master
|
|
|
|
|
* IDBAccessObject::READ_LOCKING : Select & lock the data from the master
|
|
|
|
|
*
|
|
|
|
|
* @param int $id
|
|
|
|
|
* @param int $flags (optional)
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
2017-12-28 18:34:17 +00:00
|
|
|
public function getRevisionById( $id, $flags = 0 ) {
|
|
|
|
|
return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load either the current, or a specified, revision
|
|
|
|
|
* that's attached to a given link target. If not attached
|
|
|
|
|
* to that link target, will return null.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::newFromTitle
|
|
|
|
|
*
|
|
|
|
|
* $flags include:
|
|
|
|
|
* IDBAccessObject::READ_LATEST: Select the data from the master
|
|
|
|
|
* IDBAccessObject::READ_LOCKING : Select & lock the data from the master
|
|
|
|
|
*
|
|
|
|
|
* @param LinkTarget $linkTarget
|
|
|
|
|
* @param int $revId (optional)
|
|
|
|
|
* @param int $flags Bitfield (optional)
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
|
2018-11-15 16:02:32 +00:00
|
|
|
// TODO should not require Title in future (T206498)
|
|
|
|
|
$title = Title::newFromLinkTarget( $linkTarget );
|
2017-08-27 15:29:18 +00:00
|
|
|
$conds = [
|
2018-11-15 16:02:32 +00:00
|
|
|
'page_namespace' => $title->getNamespace(),
|
|
|
|
|
'page_title' => $title->getDBkey()
|
2017-08-27 15:29:18 +00:00
|
|
|
];
|
|
|
|
|
if ( $revId ) {
|
|
|
|
|
// Use the specified revision ID.
|
|
|
|
|
// Note that we use newRevisionFromConds here because we want to retry
|
|
|
|
|
// and fall back to master if the page is not found on a replica.
|
|
|
|
|
// Since the caller supplied a revision ID, we are pretty sure the revision is
|
|
|
|
|
// supposed to exist, so we should try hard to find it.
|
|
|
|
|
$conds['rev_id'] = $revId;
|
2018-11-15 16:02:32 +00:00
|
|
|
return $this->newRevisionFromConds( $conds, $flags, $title );
|
2017-08-27 15:29:18 +00:00
|
|
|
} else {
|
|
|
|
|
// Use a join to get the latest revision.
|
|
|
|
|
// Note that we don't use newRevisionFromConds here because we don't want to retry
|
|
|
|
|
// and fall back to master. The assumption is that we only want to force the fallback
|
|
|
|
|
// if we are quite sure the revision exists because the caller supplied a revision ID.
|
|
|
|
|
// If the page isn't found at all on a replica, it probably simply does not exist.
|
2018-07-10 18:54:33 +00:00
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $flags );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
$conds[] = 'rev_id=page_latest';
|
2018-11-15 16:02:32 +00:00
|
|
|
$rev = $this->loadRevisionFromConds( $db, $conds, $flags, $title );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
return $rev;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load either the current, or a specified, revision
|
|
|
|
|
* that's attached to a given page ID.
|
|
|
|
|
* Returns null if no such revision can be found.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::newFromPageId
|
|
|
|
|
*
|
|
|
|
|
* $flags include:
|
|
|
|
|
* IDBAccessObject::READ_LATEST: Select the data from the master (since 1.20)
|
|
|
|
|
* IDBAccessObject::READ_LOCKING : Select & lock the data from the master
|
|
|
|
|
*
|
|
|
|
|
* @param int $pageId
|
|
|
|
|
* @param int $revId (optional)
|
|
|
|
|
* @param int $flags Bitfield (optional)
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
|
|
|
|
|
$conds = [ 'page_id' => $pageId ];
|
|
|
|
|
if ( $revId ) {
|
|
|
|
|
// Use the specified revision ID.
|
|
|
|
|
// Note that we use newRevisionFromConds here because we want to retry
|
|
|
|
|
// and fall back to master if the page is not found on a replica.
|
|
|
|
|
// Since the caller supplied a revision ID, we are pretty sure the revision is
|
|
|
|
|
// supposed to exist, so we should try hard to find it.
|
|
|
|
|
$conds['rev_id'] = $revId;
|
|
|
|
|
return $this->newRevisionFromConds( $conds, $flags );
|
|
|
|
|
} else {
|
|
|
|
|
// Use a join to get the latest revision.
|
|
|
|
|
// Note that we don't use newRevisionFromConds here because we don't want to retry
|
|
|
|
|
// and fall back to master. The assumption is that we only want to force the fallback
|
|
|
|
|
// if we are quite sure the revision exists because the caller supplied a revision ID.
|
|
|
|
|
// If the page isn't found at all on a replica, it probably simply does not exist.
|
2018-07-10 18:54:33 +00:00
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $flags );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
$conds[] = 'rev_id=page_latest';
|
|
|
|
|
$rev = $this->loadRevisionFromConds( $db, $conds, $flags );
|
|
|
|
|
|
|
|
|
|
return $rev;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load the revision for the given title with the given timestamp.
|
|
|
|
|
* WARNING: Timestamps may in some circumstances not be unique,
|
|
|
|
|
* so this isn't the best key to use.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::loadFromTimestamp
|
|
|
|
|
*
|
2020-03-04 01:15:53 +00:00
|
|
|
* @param LinkTarget $title
|
2017-08-27 15:29:18 +00:00
|
|
|
* @param string $timestamp
|
2020-03-04 01:15:53 +00:00
|
|
|
* @param int $flags Bitfield (optional) include:
|
|
|
|
|
* IDBAccessObject::READ_LATEST: Select the data from the master
|
|
|
|
|
* IDBAccessObject::READ_LOCKING: Select & lock the data from the master
|
|
|
|
|
* Default: IDBAccessObject::READ_NORMAL
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
2020-03-04 01:15:53 +00:00
|
|
|
public function getRevisionByTimestamp(
|
|
|
|
|
LinkTarget $title,
|
|
|
|
|
string $timestamp,
|
|
|
|
|
int $flags = IDBAccessObject::READ_NORMAL
|
|
|
|
|
): ?RevisionRecord {
|
|
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $flags );
|
2017-08-27 15:29:18 +00:00
|
|
|
return $this->newRevisionFromConds(
|
|
|
|
|
[
|
2018-03-18 01:41:45 +00:00
|
|
|
'rev_timestamp' => $db->timestamp( $timestamp ),
|
2017-08-27 15:29:18 +00:00
|
|
|
'page_namespace' => $title->getNamespace(),
|
|
|
|
|
'page_title' => $title->getDBkey()
|
|
|
|
|
],
|
2020-03-04 01:15:53 +00:00
|
|
|
$flags,
|
|
|
|
|
Title::newFromLinkTarget( $title )
|
2017-08-27 15:29:18 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
/**
|
|
|
|
|
* @param int $revId The revision to load slots for.
|
|
|
|
|
* @param int $queryFlags
|
2019-05-19 08:48:10 +00:00
|
|
|
* @param Title $title
|
2018-04-17 07:49:20 +00:00
|
|
|
*
|
|
|
|
|
* @return SlotRecord[]
|
|
|
|
|
*/
|
2019-05-19 08:48:10 +00:00
|
|
|
private function loadSlotRecords( $revId, $queryFlags, Title $title ) {
|
2018-04-17 07:49:20 +00:00
|
|
|
$revQuery = self::getSlotsQueryInfo( [ 'content' ] );
|
|
|
|
|
|
|
|
|
|
list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
|
|
|
|
|
$db = $this->getDBConnectionRef( $dbMode );
|
|
|
|
|
|
|
|
|
|
$res = $db->select(
|
|
|
|
|
$revQuery['tables'],
|
|
|
|
|
$revQuery['fields'],
|
|
|
|
|
[
|
|
|
|
|
'slot_revision_id' => $revId,
|
|
|
|
|
],
|
|
|
|
|
__METHOD__,
|
|
|
|
|
$dbOptions,
|
|
|
|
|
$revQuery['joins']
|
|
|
|
|
);
|
|
|
|
|
|
2019-05-19 08:48:10 +00:00
|
|
|
$slots = $this->constructSlotRecords( $revId, $res, $queryFlags, $title );
|
|
|
|
|
|
|
|
|
|
return $slots;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Factory method for SlotRecords based on known slot rows.
|
|
|
|
|
*
|
|
|
|
|
* @param int $revId The revision to load slots for.
|
2019-07-04 19:56:31 +00:00
|
|
|
* @param object[]|IResultWrapper $slotRows
|
2019-05-19 08:48:10 +00:00
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param Title $title
|
2019-09-18 00:25:43 +00:00
|
|
|
* @param array|null $slotContents a map from blobAddress to slot
|
|
|
|
|
* content blob or Content object.
|
2019-05-19 08:48:10 +00:00
|
|
|
*
|
|
|
|
|
* @return SlotRecord[]
|
|
|
|
|
*/
|
2019-09-18 00:25:43 +00:00
|
|
|
private function constructSlotRecords(
|
|
|
|
|
$revId,
|
|
|
|
|
$slotRows,
|
|
|
|
|
$queryFlags,
|
|
|
|
|
Title $title,
|
|
|
|
|
$slotContents = null
|
|
|
|
|
) {
|
2018-04-17 07:49:20 +00:00
|
|
|
$slots = [];
|
|
|
|
|
|
2019-05-19 08:48:10 +00:00
|
|
|
foreach ( $slotRows as $row ) {
|
|
|
|
|
// Resolve role names and model names from in-memory cache, if they were not joined in.
|
|
|
|
|
if ( !isset( $row->role_name ) ) {
|
|
|
|
|
$row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !isset( $row->model_name ) ) {
|
|
|
|
|
if ( isset( $row->content_model ) ) {
|
|
|
|
|
$row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
|
|
|
|
|
} else {
|
|
|
|
|
// We may get here if $row->model_name is set but null, perhaps because it
|
|
|
|
|
// came from rev_content_model, which is NULL for the default model.
|
|
|
|
|
$slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
|
|
|
|
|
$row->model_name = $slotRoleHandler->getDefaultModel( $title );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 12:23:40 +00:00
|
|
|
// We may have a fake blob_data field from getSlotRowsForBatch(), use it!
|
|
|
|
|
if ( isset( $row->blob_data ) ) {
|
|
|
|
|
$slotContents[$row->content_address] = $row->blob_data;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-18 00:25:43 +00:00
|
|
|
$contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
|
|
|
|
|
$blob = null;
|
|
|
|
|
if ( isset( $slotContents[$slot->getAddress()] ) ) {
|
|
|
|
|
$blob = $slotContents[$slot->getAddress()];
|
|
|
|
|
if ( $blob instanceof Content ) {
|
|
|
|
|
return $blob;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
|
2018-04-17 07:49:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-24 21:10:08 +00:00
|
|
|
if ( !isset( $slots[SlotRecord::MAIN] ) ) {
|
2018-04-17 07:49:20 +00:00
|
|
|
throw new RevisionAccessException(
|
|
|
|
|
'Main slot of revision ' . $revId . ' not found in database!'
|
|
|
|
|
);
|
2019-06-12 13:35:59 +00:00
|
|
|
}
|
2018-04-17 07:49:20 +00:00
|
|
|
|
|
|
|
|
return $slots;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-05-19 08:48:10 +00:00
|
|
|
* Factory method for RevisionSlots based on a revision ID.
|
2018-04-17 07:49:20 +00:00
|
|
|
*
|
|
|
|
|
* @note If other code has a need to construct RevisionSlots objects, this should be made
|
|
|
|
|
* public, since RevisionSlots instances should not be constructed directly.
|
|
|
|
|
*
|
|
|
|
|
* @param int $revId
|
|
|
|
|
* @param object $revisionRow
|
2019-05-19 08:48:10 +00:00
|
|
|
* @param object[]|null $slotRows
|
2018-04-17 07:49:20 +00:00
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param Title $title
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionSlots
|
|
|
|
|
* @throws MWException
|
|
|
|
|
*/
|
|
|
|
|
private function newRevisionSlots(
|
|
|
|
|
$revId,
|
|
|
|
|
$revisionRow,
|
2019-05-19 08:48:10 +00:00
|
|
|
$slotRows,
|
2018-04-17 07:49:20 +00:00
|
|
|
$queryFlags,
|
|
|
|
|
Title $title
|
|
|
|
|
) {
|
2019-05-19 08:48:10 +00:00
|
|
|
if ( $slotRows ) {
|
|
|
|
|
$slots = new RevisionSlots(
|
|
|
|
|
$this->constructSlotRecords( $revId, $slotRows, $queryFlags, $title )
|
|
|
|
|
);
|
2018-04-17 07:49:20 +00:00
|
|
|
} else {
|
|
|
|
|
// XXX: do we need the same kind of caching here
|
|
|
|
|
// that getKnownCurrentRevision uses (if $revId == page_latest?)
|
|
|
|
|
|
2019-05-19 08:48:10 +00:00
|
|
|
$slots = new RevisionSlots( function () use( $revId, $queryFlags, $title ) {
|
|
|
|
|
return $this->loadSlotRecords( $revId, $queryFlags, $title );
|
2018-04-17 07:49:20 +00:00
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $slots;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
|
|
|
|
* Make a fake revision object from an archive table row. This is queried
|
|
|
|
|
* for permissions or even inserted (as in Special:Undelete)
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::newFromArchiveRow
|
|
|
|
|
*
|
|
|
|
|
* @param object $row
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param Title|null $title
|
|
|
|
|
* @param array $overrides associative array with fields of $row to override. This may be
|
|
|
|
|
* used e.g. to force the parent revision ID or page ID. Keys in the array are fields
|
|
|
|
|
* names from the archive table without the 'ar_' prefix, i.e. use 'parent_id' to
|
|
|
|
|
* override ar_parent_id.
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionRecord
|
|
|
|
|
* @throws MWException
|
|
|
|
|
*/
|
|
|
|
|
public function newRevisionFromArchiveRow(
|
|
|
|
|
$row,
|
|
|
|
|
$queryFlags = 0,
|
|
|
|
|
Title $title = null,
|
|
|
|
|
array $overrides = []
|
2020-03-30 20:20:27 +00:00
|
|
|
) {
|
|
|
|
|
return $this->newRevisionFromArchiveRowAndSlots( $row, null, $queryFlags, $title, $overrides );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @see RevisionFactory::newRevisionFromRow
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::newFromRow
|
|
|
|
|
*
|
|
|
|
|
* @param object $row A database row generated from a query based on getQueryInfo()
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param Title|null $title Preloaded title object based on Title::newFromRow from database row
|
|
|
|
|
* when query was build with option 'page' on getQueryInfo
|
|
|
|
|
* @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale
|
|
|
|
|
* data is returned from getters, by querying the database as needed
|
|
|
|
|
* @return RevisionRecord
|
|
|
|
|
*/
|
|
|
|
|
public function newRevisionFromRow(
|
|
|
|
|
$row,
|
|
|
|
|
$queryFlags = 0,
|
|
|
|
|
Title $title = null,
|
|
|
|
|
$fromCache = false
|
|
|
|
|
) {
|
|
|
|
|
return $this->newRevisionFromRowAndSlots( $row, null, $queryFlags, $title, $fromCache );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @see newRevisionFromArchiveRow()
|
|
|
|
|
* @since 1.35
|
|
|
|
|
*
|
|
|
|
|
* @param object $row
|
|
|
|
|
* @param null|object[]|RevisionSlots $slots
|
|
|
|
|
* - Database rows generated from a query based on getSlotsQueryInfo
|
|
|
|
|
* with the 'content' flag set. Or
|
|
|
|
|
* - RevisionSlots instance
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param Title|null $title
|
|
|
|
|
* @param array $overrides associative array with fields of $row to override. This may be
|
|
|
|
|
* used e.g. to force the parent revision ID or page ID. Keys in the array are fields
|
|
|
|
|
* names from the archive table without the 'ar_' prefix, i.e. use 'parent_id' to
|
|
|
|
|
* override ar_parent_id.
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionRecord
|
|
|
|
|
* @throws MWException
|
|
|
|
|
*/
|
|
|
|
|
public function newRevisionFromArchiveRowAndSlots(
|
|
|
|
|
$row,
|
|
|
|
|
$slots,
|
|
|
|
|
$queryFlags = 0,
|
|
|
|
|
Title $title = null,
|
|
|
|
|
array $overrides = []
|
2017-08-27 15:29:18 +00:00
|
|
|
) {
|
|
|
|
|
Assert::parameterType( 'object', $row, '$row' );
|
|
|
|
|
|
|
|
|
|
// check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
|
|
|
|
|
Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
|
|
|
|
|
|
|
|
|
|
if ( !$title && isset( $overrides['title'] ) ) {
|
|
|
|
|
if ( !( $overrides['title'] instanceof Title ) ) {
|
|
|
|
|
throw new MWException( 'title field override must contain a Title object.' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$title = $overrides['title'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !isset( $title ) ) {
|
|
|
|
|
if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
|
|
|
|
|
$title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
|
|
|
|
|
} else {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
'A Title or ar_namespace and ar_title must be given'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $overrides as $key => $value ) {
|
|
|
|
|
$field = "ar_$key";
|
|
|
|
|
$row->$field = $value;
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-12 17:12:29 +00:00
|
|
|
try {
|
|
|
|
|
$user = User::newFromAnyId(
|
2017-10-06 22:17:58 +00:00
|
|
|
$row->ar_user ?? null,
|
|
|
|
|
$row->ar_user_text ?? null,
|
2019-05-03 01:57:40 +00:00
|
|
|
$row->ar_actor ?? null,
|
2019-06-27 01:33:18 +00:00
|
|
|
$this->dbDomain
|
2017-09-12 17:12:29 +00:00
|
|
|
);
|
|
|
|
|
} catch ( InvalidArgumentException $ex ) {
|
2018-05-28 19:19:11 +00:00
|
|
|
wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
|
|
|
|
|
$user = new UserIdentityValue( 0, 'Unknown user', 0 );
|
2017-09-12 17:12:29 +00:00
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2019-11-26 20:59:00 +00:00
|
|
|
if ( $user->getName() === '' ) {
|
|
|
|
|
// T236624: If the user name is empty, force 'Unknown user',
|
|
|
|
|
// even if the actor table has an entry for the empty user name.
|
|
|
|
|
$user = new UserIdentityValue( 0, 'Unknown user', 0 );
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-10 18:54:33 +00:00
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
|
|
|
|
|
// Legacy because $row may have come from self::selectFields()
|
|
|
|
|
$comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2020-03-30 20:20:27 +00:00
|
|
|
if ( !( $slots instanceof RevisionSlots ) ) {
|
|
|
|
|
$slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags, $title );
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2019-06-27 01:33:18 +00:00
|
|
|
return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->dbDomain );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-03-30 20:20:27 +00:00
|
|
|
* @see newFromRevisionRow()
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
2019-05-19 08:48:10 +00:00
|
|
|
* @param object $row A database row generated from a query based on getQueryInfo()
|
2019-09-18 00:25:43 +00:00
|
|
|
* @param null|object[]|RevisionSlots $slots
|
2020-03-30 20:20:27 +00:00
|
|
|
* - Database rows generated from a query based on getSlotsQueryInfo
|
|
|
|
|
* with the 'content' flag set. Or
|
2019-09-18 00:25:43 +00:00
|
|
|
* - RevisionSlots instance
|
2019-05-19 08:48:10 +00:00
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param Title|null $title
|
|
|
|
|
* @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale
|
|
|
|
|
* data is returned from getters, by querying the database as needed
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionRecord
|
|
|
|
|
* @throws MWException
|
|
|
|
|
* @see RevisionFactory::newRevisionFromRow
|
|
|
|
|
*/
|
|
|
|
|
public function newRevisionFromRowAndSlots(
|
|
|
|
|
$row,
|
2019-09-18 00:25:43 +00:00
|
|
|
$slots,
|
2019-05-19 08:48:10 +00:00
|
|
|
$queryFlags = 0,
|
|
|
|
|
Title $title = null,
|
|
|
|
|
$fromCache = false
|
2019-02-27 21:26:17 +00:00
|
|
|
) {
|
2017-08-27 15:29:18 +00:00
|
|
|
Assert::parameterType( 'object', $row, '$row' );
|
|
|
|
|
|
|
|
|
|
if ( !$title ) {
|
2020-04-28 14:50:03 +00:00
|
|
|
$pageId = (int)( $row->rev_page ?? 0 ); // XXX: fall back to page_id?
|
|
|
|
|
$revId = (int)( $row->rev_id ?? 0 );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2017-12-28 13:14:39 +00:00
|
|
|
$title = $this->getTitle( $pageId, $revId, $queryFlags );
|
2020-04-09 12:06:22 +00:00
|
|
|
} else {
|
|
|
|
|
$this->ensureRevisionRowMatchesTitle( $row, $title );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !isset( $row->page_latest ) ) {
|
|
|
|
|
$row->page_latest = $title->getLatestRevID();
|
|
|
|
|
if ( $row->page_latest === 0 && $title->exists() ) {
|
|
|
|
|
wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-12 17:12:29 +00:00
|
|
|
try {
|
|
|
|
|
$user = User::newFromAnyId(
|
2017-10-06 22:17:58 +00:00
|
|
|
$row->rev_user ?? null,
|
|
|
|
|
$row->rev_user_text ?? null,
|
2019-05-03 01:57:40 +00:00
|
|
|
$row->rev_actor ?? null,
|
2019-06-27 01:33:18 +00:00
|
|
|
$this->dbDomain
|
2017-09-12 17:12:29 +00:00
|
|
|
);
|
|
|
|
|
} catch ( InvalidArgumentException $ex ) {
|
2018-05-28 19:19:11 +00:00
|
|
|
wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
|
|
|
|
|
$user = new UserIdentityValue( 0, 'Unknown user', 0 );
|
2017-09-12 17:12:29 +00:00
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-07-10 18:54:33 +00:00
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
|
|
|
|
|
// Legacy because $row may have come from self::selectFields()
|
|
|
|
|
$comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2019-09-18 00:25:43 +00:00
|
|
|
if ( !( $slots instanceof RevisionSlots ) ) {
|
|
|
|
|
$slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
|
|
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2019-02-27 21:26:17 +00:00
|
|
|
// If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
|
|
|
|
|
if ( $fromCache ) {
|
|
|
|
|
$rev = new RevisionStoreCacheRecord(
|
|
|
|
|
function ( $revId ) use ( $queryFlags ) {
|
|
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
|
|
|
|
|
return $this->fetchRevisionRowFromConds(
|
|
|
|
|
$db,
|
|
|
|
|
[ 'rev_id' => intval( $revId ) ]
|
|
|
|
|
);
|
|
|
|
|
},
|
2019-06-27 01:33:18 +00:00
|
|
|
$title, $user, $comment, $row, $slots, $this->dbDomain
|
2019-02-27 21:26:17 +00:00
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$rev = new RevisionStoreRecord(
|
2019-06-27 01:33:18 +00:00
|
|
|
$title, $user, $comment, $row, $slots, $this->dbDomain );
|
2019-02-27 21:26:17 +00:00
|
|
|
}
|
|
|
|
|
return $rev;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
2020-04-09 12:06:22 +00:00
|
|
|
/**
|
|
|
|
|
* Check that the given row matches the given Title object.
|
|
|
|
|
* When a mismatch is detected, this tries to re-load the title from master,
|
|
|
|
|
* to avoid spurious errors during page moves.
|
|
|
|
|
*
|
|
|
|
|
* @param object $row
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @param array $context
|
|
|
|
|
*/
|
|
|
|
|
private function ensureRevisionRowMatchesTitle( $row, Title $title, $context = [] ) {
|
2020-04-28 14:50:03 +00:00
|
|
|
$revId = (int)( $row->rev_id ?? 0 );
|
|
|
|
|
$revPageId = (int)( $row->rev_page ?? 0 ); // XXX: also check $row->page_id?
|
2020-04-09 12:06:22 +00:00
|
|
|
$titlePageId = $title->getArticleID();
|
|
|
|
|
|
|
|
|
|
// Avoid fatal error when the Title's ID changed, T246720
|
|
|
|
|
if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
|
|
|
|
|
$masterPageId = $title->getArticleID( Title::READ_LATEST );
|
|
|
|
|
$masterLatest = $title->getLatestRevID( Title::READ_LATEST );
|
|
|
|
|
|
|
|
|
|
if ( $revPageId === $masterPageId ) {
|
|
|
|
|
$this->logger->warning(
|
|
|
|
|
"Encountered stale Title object",
|
|
|
|
|
[
|
|
|
|
|
'page_id_stale' => $titlePageId,
|
|
|
|
|
'page_id_reloaded' => $masterPageId,
|
|
|
|
|
'page_latest' => $masterLatest,
|
|
|
|
|
'rev_id' => $revId,
|
|
|
|
|
'trace' => wfBacktrace()
|
|
|
|
|
] + $context
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"Revision $revId belongs to page ID $revPageId, "
|
|
|
|
|
. "the provided Title object belongs to page ID $masterPageId"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-30 18:26:00 +00:00
|
|
|
/**
|
|
|
|
|
* Construct a RevisionRecord instance for each row in $rows,
|
|
|
|
|
* and return them as an associative array indexed by revision ID.
|
2020-03-30 20:20:27 +00:00
|
|
|
* Use getQueryInfo() or getArchiveQueryInfo() to construct the
|
|
|
|
|
* query that produces the rows.
|
|
|
|
|
*
|
2019-08-30 18:26:00 +00:00
|
|
|
* @param Traversable|array $rows the rows to construct revision records from
|
|
|
|
|
* @param array $options Supports the following options:
|
|
|
|
|
* 'slots' - whether metadata about revision slots should be
|
|
|
|
|
* loaded immediately. Supports falsy or truthy value as well
|
2019-09-26 12:23:40 +00:00
|
|
|
* as an explicit list of slot role names. The main slot will
|
|
|
|
|
* always be loaded.
|
2020-03-30 20:20:27 +00:00
|
|
|
* 'content' - whether the actual content of the slots should be
|
2019-09-18 00:25:43 +00:00
|
|
|
* preloaded.
|
2020-03-30 20:20:27 +00:00
|
|
|
* 'archive' - whether the rows where generated using getArchiveQueryInfo(),
|
|
|
|
|
* rather than getQueryInfo.
|
2019-08-30 18:26:00 +00:00
|
|
|
* @param int $queryFlags
|
2019-09-24 17:18:08 +00:00
|
|
|
* @param Title|null $title The title to which all the revision rows belong, if there
|
|
|
|
|
* is such a title and the caller has it handy, so we don't have to look it up again.
|
|
|
|
|
* If this parameter is given and any of the rows has a rev_page_id that is different
|
|
|
|
|
* from $title->getArticleID(), an InvalidArgumentException is thrown.
|
|
|
|
|
*
|
2019-08-30 18:26:00 +00:00
|
|
|
* @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions
|
|
|
|
|
* and an array of errors for the revisions failed to fetch.
|
|
|
|
|
*/
|
|
|
|
|
public function newRevisionsFromBatch(
|
|
|
|
|
$rows,
|
|
|
|
|
array $options = [],
|
|
|
|
|
$queryFlags = 0,
|
|
|
|
|
Title $title = null
|
|
|
|
|
) {
|
|
|
|
|
$result = new StatusValue();
|
2020-03-30 20:20:27 +00:00
|
|
|
$archiveMode = $options['archive'] ?? false;
|
|
|
|
|
|
|
|
|
|
if ( $archiveMode ) {
|
|
|
|
|
$revIdField = 'ar_rev_id';
|
|
|
|
|
} else {
|
|
|
|
|
$revIdField = 'rev_id';
|
|
|
|
|
}
|
2019-08-30 18:26:00 +00:00
|
|
|
|
|
|
|
|
$rowsByRevId = [];
|
2019-09-25 20:17:38 +00:00
|
|
|
$pageIdsToFetchTitles = [];
|
2020-03-30 20:20:27 +00:00
|
|
|
$titlesByPageKey = [];
|
2019-08-30 18:26:00 +00:00
|
|
|
foreach ( $rows as $row ) {
|
2020-03-30 20:20:27 +00:00
|
|
|
if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
|
2019-09-24 17:39:54 +00:00
|
|
|
$result->warning(
|
|
|
|
|
'internalerror',
|
2020-03-30 20:20:27 +00:00
|
|
|
"Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
|
2019-09-24 17:39:54 +00:00
|
|
|
);
|
2019-08-30 18:26:00 +00:00
|
|
|
}
|
2020-03-30 20:20:27 +00:00
|
|
|
|
|
|
|
|
// Attach a page key to the row, so we can find and reuse Title objects easily.
|
|
|
|
|
$row->_page_key =
|
|
|
|
|
$archiveMode ? $row->ar_namespace . ':' . $row->ar_title : $row->rev_page;
|
|
|
|
|
|
|
|
|
|
if ( $title ) {
|
|
|
|
|
if ( !$archiveMode && $row->rev_page != $title->getArticleID() ) {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"Revision {$row->$revIdField} doesn't belong to page "
|
|
|
|
|
. $title->getArticleID()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $archiveMode
|
|
|
|
|
&& ( $row->ar_namespace != $title->getNamespace()
|
|
|
|
|
|| $row->ar_title !== $title->getDBkey() )
|
|
|
|
|
) {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"Revision {$row->$revIdField} doesn't belong to page "
|
|
|
|
|
. $title->getPrefixedDBkey()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
|
|
|
|
|
if ( isset( $row->page_namespace ) && isset( $row->page_title )
|
|
|
|
|
// This should always be true, but just in case we don't have a page_id
|
2019-09-25 20:17:38 +00:00
|
|
|
// set or it doesn't match rev_page, let's fetch the title again.
|
2020-03-30 20:20:27 +00:00
|
|
|
&& isset( $row->page_id ) && isset( $row->rev_page )
|
|
|
|
|
&& $row->rev_page === $row->page_id
|
2019-09-25 20:17:38 +00:00
|
|
|
) {
|
2020-03-30 20:20:27 +00:00
|
|
|
$titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
|
|
|
|
|
} elseif ( $archiveMode ) {
|
|
|
|
|
// Can't look up deleted pages by ID, but we have namespace and title
|
|
|
|
|
$titlesByPageKey[ $row->_page_key ] =
|
|
|
|
|
Title::makeTitle( $row->ar_namespace, $row->ar_title );
|
2019-09-25 20:17:38 +00:00
|
|
|
} else {
|
|
|
|
|
$pageIdsToFetchTitles[] = $row->rev_page;
|
|
|
|
|
}
|
2019-08-30 18:26:00 +00:00
|
|
|
}
|
2020-03-30 20:20:27 +00:00
|
|
|
$rowsByRevId[$row->$revIdField] = $row;
|
2019-08-30 18:26:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( empty( $rowsByRevId ) ) {
|
|
|
|
|
$result->setResult( true, [] );
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the title is not supplied, batch-fetch Title objects.
|
|
|
|
|
if ( $title ) {
|
2020-03-30 20:20:27 +00:00
|
|
|
// same logic as for $row->_page_key above
|
|
|
|
|
$pageKey = $archiveMode
|
|
|
|
|
? $title->getNamespace() . ':' . $title->getDBkey()
|
|
|
|
|
: $title->getArticleID();
|
|
|
|
|
|
|
|
|
|
$titlesByPageKey[$pageKey] = $title;
|
2019-09-25 20:17:38 +00:00
|
|
|
} elseif ( !empty( $pageIdsToFetchTitles ) ) {
|
2020-03-30 20:20:27 +00:00
|
|
|
// Note: when we fetch titles by ID, the page key is also the ID.
|
|
|
|
|
// We should never get here if $archiveMode is true.
|
|
|
|
|
Assert::invariant( !$archiveMode, 'Titles are not loaded by ID in archive mode.' );
|
|
|
|
|
|
2019-09-25 20:17:38 +00:00
|
|
|
$pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
|
|
|
|
|
foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as $t ) {
|
2020-03-30 20:20:27 +00:00
|
|
|
$titlesByPageKey[$t->getArticleID()] = $t;
|
2019-08-30 18:26:00 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-30 20:20:27 +00:00
|
|
|
// which method to use for creating RevisionRecords
|
|
|
|
|
$newRevisionRecord = [
|
|
|
|
|
$this,
|
|
|
|
|
$archiveMode ? 'newRevisionFromArchiveRowAndSlots' : 'newRevisionFromRowAndSlots'
|
|
|
|
|
];
|
|
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
if ( !isset( $options['slots'] ) ) {
|
2020-03-30 20:20:27 +00:00
|
|
|
$result->setResult(
|
|
|
|
|
true,
|
|
|
|
|
array_map(
|
|
|
|
|
function ( $row )
|
|
|
|
|
use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord ) {
|
|
|
|
|
try {
|
|
|
|
|
return $newRevisionRecord( $row, null, $queryFlags,
|
|
|
|
|
$titlesByPageKey[ $row->_page_key ] ?? null );
|
|
|
|
|
} catch ( MWException $e ) {
|
|
|
|
|
$result->warning( 'internalerror', $e->getMessage() );
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
$rowsByRevId
|
|
|
|
|
)
|
2019-08-30 18:26:00 +00:00
|
|
|
);
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 12:23:40 +00:00
|
|
|
$slotRowOptions = [
|
|
|
|
|
'slots' => $options['slots'] ?? true,
|
|
|
|
|
'blobs' => $options['content'] ?? false,
|
|
|
|
|
];
|
2019-08-30 18:26:00 +00:00
|
|
|
|
2019-09-26 12:23:40 +00:00
|
|
|
if ( is_array( $slotRowOptions['slots'] )
|
|
|
|
|
&& !in_array( SlotRecord::MAIN, $slotRowOptions['slots'] )
|
|
|
|
|
) {
|
|
|
|
|
// Make sure the main slot is always loaded, RevisionRecord requires this.
|
|
|
|
|
$slotRowOptions['slots'][] = SlotRecord::MAIN;
|
2019-08-30 18:26:00 +00:00
|
|
|
}
|
2019-09-18 00:25:43 +00:00
|
|
|
|
2019-09-26 12:23:40 +00:00
|
|
|
$slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
|
|
|
|
|
|
|
|
|
|
$result->merge( $slotRowsStatus );
|
|
|
|
|
$slotRowsByRevId = $slotRowsStatus->getValue();
|
2019-09-18 00:25:43 +00:00
|
|
|
|
2020-03-30 20:20:27 +00:00
|
|
|
$result->setResult(
|
|
|
|
|
true,
|
|
|
|
|
array_map(
|
|
|
|
|
function ( $row )
|
|
|
|
|
use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
|
|
|
|
|
$revIdField, $newRevisionRecord
|
|
|
|
|
) {
|
|
|
|
|
if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
|
|
|
|
|
$result->warning(
|
|
|
|
|
'internalerror',
|
|
|
|
|
"Couldn't find slots for rev {$row->$revIdField}"
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return $newRevisionRecord(
|
|
|
|
|
$row,
|
|
|
|
|
new RevisionSlots(
|
|
|
|
|
$this->constructSlotRecords(
|
|
|
|
|
$row->$revIdField,
|
|
|
|
|
$slotRowsByRevId[$row->$revIdField],
|
|
|
|
|
$queryFlags,
|
|
|
|
|
$titlesByPageKey[$row->_page_key] ?? null
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
$queryFlags,
|
|
|
|
|
$titlesByPageKey[$row->_page_key]
|
|
|
|
|
);
|
|
|
|
|
} catch ( MWException $e ) {
|
|
|
|
|
$result->warning( 'internalerror', $e->getMessage() );
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
$rowsByRevId
|
|
|
|
|
)
|
|
|
|
|
);
|
2019-08-30 18:26:00 +00:00
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-26 12:23:40 +00:00
|
|
|
/**
|
|
|
|
|
* Gets the slot rows associated with a batch of revisions.
|
|
|
|
|
* The serialized content of each slot can be included by setting the 'blobs' option.
|
|
|
|
|
* Callers are responsible for unserializing and interpreting the content blobs
|
|
|
|
|
* based on the model_name and role_name fields.
|
|
|
|
|
*
|
2020-03-30 20:20:27 +00:00
|
|
|
* @param Traversable|array $rowsOrIds list of revision ids, or revision or archive rows
|
|
|
|
|
* from a db query.
|
2019-09-26 12:23:40 +00:00
|
|
|
* @param array $options Supports the following options:
|
|
|
|
|
* 'slots' - a list of slot role names to fetch. If omitted or true or null,
|
|
|
|
|
* all slots are fetched
|
2020-03-30 20:20:27 +00:00
|
|
|
* 'blobs' - whether the serialized content of each slot should be loaded.
|
2019-09-26 12:23:40 +00:00
|
|
|
* If true, the serialiezd content will be present in the slot row
|
|
|
|
|
* in the blob_data field.
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
*
|
|
|
|
|
* @return StatusValue a status containing, if isOK() returns true, a two-level nested
|
|
|
|
|
* associative array, mapping from revision ID to an associative array that maps from
|
|
|
|
|
* role name to a database row object. The database row object will contain the fields
|
|
|
|
|
* defined by getSlotQueryInfo() with the 'content' flag set, plus the blob_data field
|
|
|
|
|
* if the 'blobs' is set in $options. The model_name and role_name fields will also be
|
|
|
|
|
* set.
|
|
|
|
|
*/
|
|
|
|
|
private function getSlotRowsForBatch(
|
|
|
|
|
$rowsOrIds,
|
|
|
|
|
array $options = [],
|
|
|
|
|
$queryFlags = 0
|
|
|
|
|
) {
|
|
|
|
|
$result = new StatusValue();
|
|
|
|
|
|
|
|
|
|
$revIds = [];
|
|
|
|
|
foreach ( $rowsOrIds as $row ) {
|
2020-03-30 20:20:27 +00:00
|
|
|
if ( is_object( $row ) ) {
|
|
|
|
|
$revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
|
|
|
|
|
} else {
|
|
|
|
|
$revIds[] = (int)$row;
|
|
|
|
|
}
|
2019-09-26 12:23:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nothing to do.
|
|
|
|
|
// Note that $rowsOrIds may not be "empty" even if $revIds is, e.g. if it's a ResultWrapper.
|
|
|
|
|
if ( empty( $revIds ) ) {
|
|
|
|
|
$result->setResult( true, [] );
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to set the `content` flag to join in content meta-data
|
|
|
|
|
$slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
|
|
|
|
|
$revIdField = $slotQueryInfo['keys']['rev_id'];
|
|
|
|
|
$slotQueryConds = [ $revIdField => $revIds ];
|
|
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
if ( isset( $options['slots'] ) && is_array( $options['slots'] ) ) {
|
2019-09-26 12:23:40 +00:00
|
|
|
if ( empty( $options['slots'] ) ) {
|
|
|
|
|
// Degenerate case: return no slots for each revision.
|
|
|
|
|
$result->setResult( true, array_fill_keys( $revIds, [] ) );
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$roleIdField = $slotQueryInfo['keys']['role_id'];
|
|
|
|
|
$slotQueryConds[$roleIdField] = array_map( function ( $slot_name ) {
|
|
|
|
|
return $this->slotRoleStore->getId( $slot_name );
|
|
|
|
|
}, $options['slots'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
|
|
|
|
|
$slotRows = $db->select(
|
|
|
|
|
$slotQueryInfo['tables'],
|
|
|
|
|
$slotQueryInfo['fields'],
|
|
|
|
|
$slotQueryConds,
|
|
|
|
|
__METHOD__,
|
|
|
|
|
[],
|
|
|
|
|
$slotQueryInfo['joins']
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$slotContents = null;
|
|
|
|
|
if ( $options['blobs'] ?? false ) {
|
|
|
|
|
$blobAddresses = [];
|
|
|
|
|
foreach ( $slotRows as $slotRow ) {
|
|
|
|
|
$blobAddresses[] = $slotRow->content_address;
|
|
|
|
|
}
|
|
|
|
|
$slotContentFetchStatus = $this->blobStore
|
|
|
|
|
->getBlobBatch( $blobAddresses, $queryFlags );
|
|
|
|
|
foreach ( $slotContentFetchStatus->getErrors() as $error ) {
|
|
|
|
|
$result->warning( $error['message'], ...$error['params'] );
|
|
|
|
|
}
|
|
|
|
|
$slotContents = $slotContentFetchStatus->getValue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$slotRowsByRevId = [];
|
|
|
|
|
foreach ( $slotRows as $slotRow ) {
|
|
|
|
|
if ( $slotContents === null ) {
|
|
|
|
|
// nothing to do
|
|
|
|
|
} elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
|
|
|
|
|
$slotRow->blob_data = $slotContents[$slotRow->content_address];
|
|
|
|
|
} else {
|
|
|
|
|
$result->warning(
|
|
|
|
|
'internalerror',
|
|
|
|
|
"Couldn't find blob data for rev {$slotRow->slot_revision_id}"
|
|
|
|
|
);
|
|
|
|
|
$slotRow->blob_data = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// conditional needed for SCHEMA_COMPAT_READ_OLD
|
|
|
|
|
if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
|
|
|
|
|
$slotRow->role_name = $this->slotRoleStore->getName( (int)$slotRow->slot_role_id );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// conditional needed for SCHEMA_COMPAT_READ_OLD
|
|
|
|
|
if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
|
|
|
|
|
$slotRow->model_name = $this->contentModelStore->getName( (int)$slotRow->content_model );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result->setResult( true, $slotRowsByRevId );
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets raw (serialized) content blobs for the given set of revisions.
|
|
|
|
|
* Callers are responsible for unserializing and interpreting the content blobs
|
|
|
|
|
* based on the model_name field and the slot role.
|
|
|
|
|
*
|
|
|
|
|
* This method is intended for bulk operations in maintenance scripts.
|
|
|
|
|
* It may be chosen over newRevisionsFromBatch by code that are only interested
|
|
|
|
|
* in raw content, as opposed to meta data. Code that needs to access meta data of revisions,
|
|
|
|
|
* slots, or content objects should use newRevisionsFromBatch() instead.
|
|
|
|
|
*
|
|
|
|
|
* @param Traversable|array $rowsOrIds list of revision ids, or revision rows from a db query.
|
|
|
|
|
* @param array|null $slots the role names for which to get slots.
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
*
|
|
|
|
|
* @return StatusValue a status containing, if isOK() returns true, a two-level nested
|
|
|
|
|
* associative array, mapping from revision ID to an associative array that maps from
|
|
|
|
|
* role name to an anonymous object object containing two fields:
|
|
|
|
|
* - model_name: the name of the content's model
|
|
|
|
|
* - blob_data: serialized content data
|
|
|
|
|
*/
|
|
|
|
|
public function getContentBlobsForBatch(
|
|
|
|
|
$rowsOrIds,
|
|
|
|
|
$slots = null,
|
|
|
|
|
$queryFlags = 0
|
|
|
|
|
) {
|
|
|
|
|
$result = $this->getSlotRowsForBatch(
|
|
|
|
|
$rowsOrIds,
|
|
|
|
|
[ 'slots' => $slots, 'blobs' => true ],
|
|
|
|
|
$queryFlags
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ( $result->isOK() ) {
|
|
|
|
|
// strip out all internal meta data that we don't want to expose
|
|
|
|
|
foreach ( $result->value as $revId => $rowsByRole ) {
|
|
|
|
|
foreach ( $rowsByRole as $role => $slotRow ) {
|
|
|
|
|
if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
|
|
|
|
|
// In SCHEMA_COMPAT_READ_OLD mode we may get the main slot even
|
|
|
|
|
// if we didn't ask for it.
|
|
|
|
|
unset( $result->value[$revId][$role] );
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result->value[$revId][$role] = (object)[
|
|
|
|
|
'blob_data' => $slotRow->blob_data,
|
|
|
|
|
'model_name' => $slotRow->model_name,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
|
|
|
|
* Constructs a new MutableRevisionRecord based on the given associative array following
|
|
|
|
|
* the MW1.29 convention for the Revision constructor.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::newFromRow
|
|
|
|
|
*
|
|
|
|
|
* @param array $fields
|
|
|
|
|
* @param int $queryFlags
|
|
|
|
|
* @param Title|null $title
|
|
|
|
|
*
|
|
|
|
|
* @return MutableRevisionRecord
|
|
|
|
|
* @throws MWException
|
|
|
|
|
* @throws RevisionAccessException
|
|
|
|
|
*/
|
|
|
|
|
public function newMutableRevisionFromArray(
|
|
|
|
|
array $fields,
|
|
|
|
|
$queryFlags = 0,
|
|
|
|
|
Title $title = null
|
|
|
|
|
) {
|
|
|
|
|
if ( !$title && isset( $fields['title'] ) ) {
|
|
|
|
|
if ( !( $fields['title'] instanceof Title ) ) {
|
|
|
|
|
throw new MWException( 'title field must contain a Title object.' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$title = $fields['title'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$title ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$pageId = $fields['page'] ?? 0;
|
|
|
|
|
$revId = $fields['id'] ?? 0;
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2017-12-28 13:14:39 +00:00
|
|
|
$title = $this->getTitle( $pageId, $revId, $queryFlags );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !isset( $fields['page'] ) ) {
|
|
|
|
|
$fields['page'] = $title->getArticleID( $queryFlags );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if we have a content object, use it to set the model and type
|
2019-03-29 20:12:24 +00:00
|
|
|
if ( !empty( $fields['content'] ) && !( $fields['content'] instanceof Content )
|
|
|
|
|
&& !is_array( $fields['content'] )
|
|
|
|
|
) {
|
|
|
|
|
throw new MWException(
|
|
|
|
|
'content field must contain a Content object or an array of Content objects.'
|
|
|
|
|
);
|
2018-04-17 07:49:20 +00:00
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-04-17 07:49:20 +00:00
|
|
|
if ( !empty( $fields['text_id'] ) ) {
|
2019-12-17 16:20:32 +00:00
|
|
|
throw new MWException( 'The text_id field can not be used in MediaWiki 1.35 and later' );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
isset( $fields['comment'] )
|
|
|
|
|
&& !( $fields['comment'] instanceof CommentStoreComment )
|
|
|
|
|
) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$commentData = $fields['comment_data'] ?? null;
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
if ( $fields['comment'] instanceof Message ) {
|
|
|
|
|
$fields['comment'] = CommentStoreComment::newUnsavedComment(
|
|
|
|
|
$fields['comment'],
|
|
|
|
|
$commentData
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$commentText = trim( strval( $fields['comment'] ) );
|
|
|
|
|
$fields['comment'] = CommentStoreComment::newUnsavedComment(
|
|
|
|
|
$commentText,
|
|
|
|
|
$commentData
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-27 01:33:18 +00:00
|
|
|
$revision = new MutableRevisionRecord( $title, $this->dbDomain );
|
2018-04-17 07:49:20 +00:00
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
/** @var Content[] $slotContent */
|
|
|
|
|
if ( isset( $fields['content'] ) ) {
|
|
|
|
|
if ( is_array( $fields['content'] ) ) {
|
|
|
|
|
$slotContent = $fields['content'];
|
|
|
|
|
} else {
|
|
|
|
|
$slotContent = [ SlotRecord::MAIN => $fields['content'] ];
|
|
|
|
|
}
|
|
|
|
|
} elseif ( isset( $fields['text'] ) ) {
|
|
|
|
|
if ( isset( $fields['content_model'] ) ) {
|
|
|
|
|
$model = $fields['content_model'];
|
|
|
|
|
} else {
|
|
|
|
|
$slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
|
|
|
|
|
$model = $slotRoleHandler->getDefaultModel( $title );
|
2018-04-17 07:49:20 +00:00
|
|
|
}
|
2019-12-17 16:20:32 +00:00
|
|
|
|
|
|
|
|
$contentHandler = ContentHandler::getForModelID( $model );
|
|
|
|
|
$content = $contentHandler->unserializeContent( $fields['text'] );
|
|
|
|
|
$slotContent = [ SlotRecord::MAIN => $content ];
|
2018-04-17 07:49:20 +00:00
|
|
|
} else {
|
2019-12-17 16:20:32 +00:00
|
|
|
$slotContent = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $slotContent as $role => $content ) {
|
|
|
|
|
$revision->setContent( $role, $content );
|
2018-04-17 07:49:20 +00:00
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
$this->initializeMutableRevisionFromArray( $revision, $fields );
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
return $revision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param MutableRevisionRecord $record
|
|
|
|
|
* @param array $fields
|
|
|
|
|
*/
|
|
|
|
|
private function initializeMutableRevisionFromArray(
|
|
|
|
|
MutableRevisionRecord $record,
|
|
|
|
|
array $fields
|
|
|
|
|
) {
|
|
|
|
|
/** @var UserIdentity $user */
|
|
|
|
|
$user = null;
|
|
|
|
|
|
2019-05-03 01:57:40 +00:00
|
|
|
// If a user is passed in, use it if possible. We cannot use a user from a
|
|
|
|
|
// remote wiki with unsuppressed ids, due to issues described in T222212.
|
|
|
|
|
if ( isset( $fields['user'] ) &&
|
|
|
|
|
( $fields['user'] instanceof UserIdentity ) &&
|
2019-06-27 01:33:18 +00:00
|
|
|
( $this->dbDomain === false ||
|
2019-05-03 01:57:40 +00:00
|
|
|
( !$fields['user']->getId() && !$fields['user']->getActorId() ) )
|
|
|
|
|
) {
|
2017-08-27 15:29:18 +00:00
|
|
|
$user = $fields['user'];
|
2017-09-12 17:12:29 +00:00
|
|
|
} else {
|
2020-01-24 20:14:50 +00:00
|
|
|
$userID = isset( $fields['user'] ) && is_numeric( $fields['user'] ) ? $fields['user'] : null;
|
2017-09-12 17:12:29 +00:00
|
|
|
try {
|
|
|
|
|
$user = User::newFromAnyId(
|
2020-01-24 20:14:50 +00:00
|
|
|
$userID,
|
2017-10-06 22:17:58 +00:00
|
|
|
$fields['user_text'] ?? null,
|
2019-05-03 01:57:40 +00:00
|
|
|
$fields['actor'] ?? null,
|
2019-06-27 01:33:18 +00:00
|
|
|
$this->dbDomain
|
2017-09-12 17:12:29 +00:00
|
|
|
);
|
|
|
|
|
} catch ( InvalidArgumentException $ex ) {
|
|
|
|
|
$user = null;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $user ) {
|
|
|
|
|
$record->setUser( $user );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$timestamp = isset( $fields['timestamp'] )
|
|
|
|
|
? strval( $fields['timestamp'] )
|
2020-04-02 20:00:24 +00:00
|
|
|
: MWTimestamp::now( TS_MW );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
$record->setTimestamp( $timestamp );
|
|
|
|
|
|
|
|
|
|
if ( isset( $fields['page'] ) ) {
|
|
|
|
|
$record->setPageId( intval( $fields['page'] ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $fields['id'] ) ) {
|
|
|
|
|
$record->setId( intval( $fields['id'] ) );
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $fields['parent_id'] ) ) {
|
|
|
|
|
$record->setParentId( intval( $fields['parent_id'] ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $fields['sha1'] ) ) {
|
|
|
|
|
$record->setSha1( $fields['sha1'] );
|
|
|
|
|
}
|
2019-12-17 16:20:32 +00:00
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
if ( isset( $fields['size'] ) ) {
|
|
|
|
|
$record->setSize( intval( $fields['size'] ) );
|
2019-12-17 16:20:32 +00:00
|
|
|
} elseif ( isset( $fields['len'] ) ) {
|
|
|
|
|
$record->setSize( intval( $fields['len'] ) );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $fields['minor_edit'] ) ) {
|
|
|
|
|
$record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $fields['deleted'] ) ) {
|
|
|
|
|
$record->setVisibility( intval( $fields['deleted'] ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $fields['comment'] ) ) {
|
|
|
|
|
Assert::parameterType(
|
|
|
|
|
CommentStoreComment::class,
|
|
|
|
|
$fields['comment'],
|
|
|
|
|
'$row[\'comment\']'
|
|
|
|
|
);
|
|
|
|
|
$record->setComment( $fields['comment'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load either the current, or a specified, revision
|
|
|
|
|
* that's attached to a given page. If not attached
|
|
|
|
|
* to that page, will return null.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::loadFromPageId
|
|
|
|
|
*
|
2020-03-04 01:02:30 +00:00
|
|
|
* @deprecated since 1.35 Use RevisionStore::getRevisionByPageId instead.
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param int $pageid
|
|
|
|
|
* @param int $id
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
|
2020-03-04 01:02:30 +00:00
|
|
|
wfDeprecated( __METHOD__, '1.35' );
|
2017-08-27 15:29:18 +00:00
|
|
|
$conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
|
|
|
|
|
if ( $id ) {
|
|
|
|
|
$conds['rev_id'] = intval( $id );
|
|
|
|
|
} else {
|
|
|
|
|
$conds[] = 'rev_id=page_latest';
|
|
|
|
|
}
|
|
|
|
|
return $this->loadRevisionFromConds( $db, $conds );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load either the current, or a specified, revision
|
|
|
|
|
* that's attached to a given page. If not attached
|
|
|
|
|
* to that page, will return null.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::loadFromTitle
|
|
|
|
|
*
|
|
|
|
|
* @note direct use is deprecated!
|
|
|
|
|
* @todo remove when unused!
|
2020-02-29 00:22:03 +00:00
|
|
|
* @deprecated since 1.35
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @param int $id
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
|
2020-02-29 00:22:03 +00:00
|
|
|
wfDeprecated( __METHOD__, '1.35' );
|
2017-08-27 15:29:18 +00:00
|
|
|
if ( $id ) {
|
|
|
|
|
$matchId = intval( $id );
|
|
|
|
|
} else {
|
|
|
|
|
$matchId = 'page_latest';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->loadRevisionFromConds(
|
|
|
|
|
$db,
|
|
|
|
|
[
|
|
|
|
|
"rev_id=$matchId",
|
|
|
|
|
'page_namespace' => $title->getNamespace(),
|
|
|
|
|
'page_title' => $title->getDBkey()
|
|
|
|
|
],
|
|
|
|
|
0,
|
|
|
|
|
$title
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load the revision for the given title with the given timestamp.
|
|
|
|
|
* WARNING: Timestamps may in some circumstances not be unique,
|
|
|
|
|
* so this isn't the best key to use.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::loadFromTimestamp
|
|
|
|
|
*
|
2020-03-04 01:39:36 +00:00
|
|
|
* @deprecated since 1.35
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @param string $timestamp
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
|
2020-03-04 01:39:36 +00:00
|
|
|
wfDeprecated( __METHOD__, '1.35' );
|
2017-08-27 15:29:18 +00:00
|
|
|
return $this->loadRevisionFromConds( $db,
|
|
|
|
|
[
|
|
|
|
|
'rev_timestamp' => $db->timestamp( $timestamp ),
|
|
|
|
|
'page_namespace' => $title->getNamespace(),
|
|
|
|
|
'page_title' => $title->getDBkey()
|
|
|
|
|
],
|
|
|
|
|
0,
|
|
|
|
|
$title
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Given a set of conditions, fetch a revision
|
|
|
|
|
*
|
|
|
|
|
* This method should be used if we are pretty sure the revision exists.
|
|
|
|
|
* Unless $flags has READ_LATEST set, this method will first try to find the revision
|
|
|
|
|
* on a replica before hitting the master database.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this corresponds to Revision::newFromConds
|
|
|
|
|
*
|
|
|
|
|
* @param array $conditions
|
|
|
|
|
* @param int $flags (optional)
|
2020-03-07 07:07:42 +00:00
|
|
|
* @param Title|null $title (optional)
|
|
|
|
|
* @param array $options (optional) additional query options
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
2020-03-07 07:07:42 +00:00
|
|
|
private function newRevisionFromConds(
|
|
|
|
|
array $conditions,
|
|
|
|
|
int $flags = IDBAccessObject::READ_NORMAL,
|
|
|
|
|
Title $title = null,
|
|
|
|
|
array $options = []
|
|
|
|
|
) {
|
2018-07-10 18:54:33 +00:00
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $flags );
|
2020-03-07 07:07:42 +00:00
|
|
|
$rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title, $options );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
$lb = $this->getDBLoadBalancer();
|
|
|
|
|
|
|
|
|
|
// Make sure new pending/committed revision are visibile later on
|
|
|
|
|
// within web requests to certain avoid bugs like T93866 and T94407.
|
|
|
|
|
if ( !$rev
|
|
|
|
|
&& !( $flags & self::READ_LATEST )
|
2019-06-18 21:12:06 +00:00
|
|
|
&& $lb->hasStreamingReplicaServers()
|
2017-08-27 15:29:18 +00:00
|
|
|
&& $lb->hasOrMadeRecentMasterChanges()
|
|
|
|
|
) {
|
|
|
|
|
$flags = self::READ_LATEST;
|
2019-08-04 14:50:50 +00:00
|
|
|
$dbw = $this->getDBConnectionRef( DB_MASTER );
|
2020-03-07 07:07:42 +00:00
|
|
|
$rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title, $options );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $rev;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Given a set of conditions, fetch a revision from
|
|
|
|
|
* the given database connection.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this corresponds to Revision::loadFromConds
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param array $conditions
|
|
|
|
|
* @param int $flags (optional)
|
2020-03-07 07:07:42 +00:00
|
|
|
* @param Title|null $title (optional) additional query options
|
|
|
|
|
* @param array $options (optional) additional query options
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
private function loadRevisionFromConds(
|
|
|
|
|
IDatabase $db,
|
2020-03-07 07:07:42 +00:00
|
|
|
array $conditions,
|
|
|
|
|
int $flags = IDBAccessObject::READ_NORMAL,
|
|
|
|
|
Title $title = null,
|
|
|
|
|
array $options = []
|
2017-08-27 15:29:18 +00:00
|
|
|
) {
|
2020-03-07 07:07:42 +00:00
|
|
|
$row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
|
2017-08-27 15:29:18 +00:00
|
|
|
if ( $row ) {
|
|
|
|
|
$rev = $this->newRevisionFromRow( $row, $flags, $title );
|
|
|
|
|
|
|
|
|
|
return $rev;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Throws an exception if the given database connection does not belong to the wiki this
|
|
|
|
|
* RevisionStore is bound to.
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @throws MWException
|
|
|
|
|
*/
|
2019-07-04 07:46:39 +00:00
|
|
|
private function checkDatabaseDomain( IDatabase $db ) {
|
|
|
|
|
$dbDomain = $db->getDomainID();
|
|
|
|
|
$storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
|
|
|
|
|
if ( $dbDomain === $storeDomain ) {
|
2018-01-03 14:42:13 +00:00
|
|
|
return;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
2018-01-03 14:42:13 +00:00
|
|
|
|
2019-07-04 07:46:39 +00:00
|
|
|
throw new MWException( "DB connection domain '$dbDomain' does not match '$storeDomain'" );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Given a set of conditions, return a row with the
|
|
|
|
|
* fields necessary to build RevisionRecord objects.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this corresponds to Revision::fetchFromConds
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param array $conditions
|
|
|
|
|
* @param int $flags (optional)
|
2020-03-07 07:07:42 +00:00
|
|
|
* @param array $options (optional) additional query options
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @return object|false data row as a raw object
|
|
|
|
|
*/
|
2020-03-07 07:07:42 +00:00
|
|
|
private function fetchRevisionRowFromConds(
|
|
|
|
|
IDatabase $db,
|
|
|
|
|
array $conditions,
|
|
|
|
|
int $flags = IDBAccessObject::READ_NORMAL,
|
|
|
|
|
array $options = []
|
|
|
|
|
) {
|
2019-07-04 07:46:39 +00:00
|
|
|
$this->checkDatabaseDomain( $db );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2018-01-29 15:54:02 +00:00
|
|
|
$revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
|
2017-08-27 15:29:18 +00:00
|
|
|
if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
|
|
|
|
|
$options[] = 'FOR UPDATE';
|
|
|
|
|
}
|
|
|
|
|
return $db->selectRow(
|
|
|
|
|
$revQuery['tables'],
|
|
|
|
|
$revQuery['fields'],
|
|
|
|
|
$conditions,
|
|
|
|
|
__METHOD__,
|
|
|
|
|
$options,
|
|
|
|
|
$revQuery['joins']
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the tables, fields, and join conditions to be selected to create
|
2018-04-17 07:49:20 +00:00
|
|
|
* a new RevisionStoreRecord object.
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::getQueryInfo
|
|
|
|
|
*
|
2018-01-29 15:54:02 +00:00
|
|
|
* If the format of fields returned changes in any way then the cache key provided by
|
|
|
|
|
* self::getRevisionRowCacheKey should be updated.
|
|
|
|
|
*
|
2017-08-27 15:29:18 +00:00
|
|
|
* @since 1.31
|
|
|
|
|
*
|
|
|
|
|
* @param array $options Any combination of the following strings
|
|
|
|
|
* - 'page': Join with the page table, and select fields to identify the page
|
|
|
|
|
* - 'user': Join with the user table, and select the user name
|
|
|
|
|
*
|
|
|
|
|
* @return array With three keys:
|
|
|
|
|
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
|
|
|
|
|
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
|
|
|
|
|
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
|
2019-08-30 18:17:32 +00:00
|
|
|
* @phan-return array{tables:string[],fields:string[],joins:array}
|
2017-08-27 15:29:18 +00:00
|
|
|
*/
|
|
|
|
|
public function getQueryInfo( $options = [] ) {
|
|
|
|
|
$ret = [
|
|
|
|
|
'tables' => [],
|
|
|
|
|
'fields' => [],
|
|
|
|
|
'joins' => [],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$ret['tables'][] = 'revision';
|
|
|
|
|
$ret['fields'] = array_merge( $ret['fields'], [
|
|
|
|
|
'rev_id',
|
|
|
|
|
'rev_page',
|
|
|
|
|
'rev_timestamp',
|
|
|
|
|
'rev_minor_edit',
|
|
|
|
|
'rev_deleted',
|
|
|
|
|
'rev_len',
|
|
|
|
|
'rev_parent_id',
|
|
|
|
|
'rev_sha1',
|
|
|
|
|
] );
|
|
|
|
|
|
2018-01-29 14:25:49 +00:00
|
|
|
$commentQuery = $this->commentStore->getJoin( 'rev_comment' );
|
2017-08-27 15:29:18 +00:00
|
|
|
$ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
|
|
|
|
|
$ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
|
|
|
|
|
$ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
|
|
|
|
|
|
2017-09-12 17:12:29 +00:00
|
|
|
$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'] );
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
if ( in_array( 'page', $options, true ) ) {
|
|
|
|
|
$ret['tables'][] = 'page';
|
|
|
|
|
$ret['fields'] = array_merge( $ret['fields'], [
|
|
|
|
|
'page_namespace',
|
|
|
|
|
'page_title',
|
|
|
|
|
'page_id',
|
|
|
|
|
'page_latest',
|
|
|
|
|
'page_is_redirect',
|
|
|
|
|
'page_len',
|
|
|
|
|
] );
|
2019-03-06 17:17:27 +00:00
|
|
|
$ret['joins']['page'] = [ 'JOIN', [ 'page_id = rev_page' ] ];
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'user', $options, true ) ) {
|
|
|
|
|
$ret['tables'][] = 'user';
|
|
|
|
|
$ret['fields'] = array_merge( $ret['fields'], [
|
|
|
|
|
'user_name',
|
|
|
|
|
] );
|
2017-09-12 17:12:29 +00:00
|
|
|
$u = $actorQuery['fields']['rev_user'];
|
|
|
|
|
$ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'text', $options, true ) ) {
|
2019-12-17 16:20:32 +00:00
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
'The `text` option is no longer supported in MediaWiki 1.35 and later.'
|
|
|
|
|
);
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the tables, fields, and join conditions to be selected to create
|
2018-04-17 07:49:20 +00:00
|
|
|
* a new SlotRecord.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.32
|
|
|
|
|
*
|
|
|
|
|
* @param array $options Any combination of the following strings
|
|
|
|
|
* - 'content': Join with the content table, and select content meta-data fields
|
2018-08-22 16:08:23 +00:00
|
|
|
* - 'model': Join with the content_models table, and select the model_name field.
|
|
|
|
|
* Only applicable if 'content' is also set.
|
|
|
|
|
* - 'role': Join with the slot_roles table, and select the role_name field
|
2018-04-17 07:49:20 +00:00
|
|
|
*
|
|
|
|
|
* @return array With three keys:
|
|
|
|
|
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
|
|
|
|
|
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
|
|
|
|
|
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
|
2019-09-26 12:23:40 +00:00
|
|
|
* - keys: (associative array) to look up fields to match against.
|
|
|
|
|
* In particular, the field that can be used to find slots by rev_id
|
|
|
|
|
* can be found in ['keys']['rev_id'].
|
2018-04-17 07:49:20 +00:00
|
|
|
*/
|
|
|
|
|
public function getSlotsQueryInfo( $options = [] ) {
|
|
|
|
|
$ret = [
|
|
|
|
|
'tables' => [],
|
|
|
|
|
'fields' => [],
|
|
|
|
|
'joins' => [],
|
2019-09-26 12:23:40 +00:00
|
|
|
'keys' => [],
|
2018-04-17 07:49:20 +00:00
|
|
|
];
|
|
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
$ret['keys']['rev_id'] = 'slot_revision_id';
|
|
|
|
|
$ret['keys']['role_id'] = 'slot_role_id';
|
2018-04-17 07:49:20 +00:00
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
$ret['tables'][] = 'slots';
|
|
|
|
|
$ret['fields'] = array_merge( $ret['fields'], [
|
|
|
|
|
'slot_revision_id',
|
|
|
|
|
'slot_content_id',
|
|
|
|
|
'slot_origin',
|
|
|
|
|
'slot_role_id',
|
|
|
|
|
] );
|
2019-05-19 08:48:10 +00:00
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
if ( in_array( 'role', $options, true ) ) {
|
|
|
|
|
// Use left join to attach role name, so we still find the revision row even
|
|
|
|
|
// if the role name is missing. This triggers a more obvious failure mode.
|
|
|
|
|
$ret['tables'][] = 'slot_roles';
|
|
|
|
|
$ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
|
|
|
|
|
$ret['fields'][] = 'role_name';
|
|
|
|
|
}
|
2018-04-17 07:49:20 +00:00
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
if ( in_array( 'content', $options, true ) ) {
|
|
|
|
|
$ret['keys']['model_id'] = 'content_model';
|
2019-09-26 12:23:40 +00:00
|
|
|
|
2019-12-17 16:20:32 +00:00
|
|
|
$ret['tables'][] = 'content';
|
2018-04-17 07:49:20 +00:00
|
|
|
$ret['fields'] = array_merge( $ret['fields'], [
|
2019-12-17 16:20:32 +00:00
|
|
|
'content_size',
|
|
|
|
|
'content_sha1',
|
|
|
|
|
'content_address',
|
|
|
|
|
'content_model',
|
2018-04-17 07:49:20 +00:00
|
|
|
] );
|
2019-12-17 16:20:32 +00:00
|
|
|
$ret['joins']['content'] = [ 'JOIN', [ 'slot_content_id = content_id' ] ];
|
|
|
|
|
|
|
|
|
|
if ( in_array( 'model', $options, true ) ) {
|
|
|
|
|
// Use left join to attach model name, so we still find the revision row even
|
|
|
|
|
// if the model name is missing. This triggers a more obvious failure mode.
|
|
|
|
|
$ret['tables'][] = 'content_models';
|
|
|
|
|
$ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
|
|
|
|
|
$ret['fields'][] = 'model_name';
|
2018-08-22 16:08:23 +00:00
|
|
|
}
|
2018-04-17 07:49:20 +00:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the tables, fields, and join conditions to be selected to create
|
|
|
|
|
* a new RevisionArchiveRecord object.
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::getArchiveQueryInfo
|
|
|
|
|
*
|
|
|
|
|
* @since 1.31
|
|
|
|
|
*
|
|
|
|
|
* @return array With three keys:
|
|
|
|
|
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
|
|
|
|
|
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
|
|
|
|
|
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
|
|
|
|
|
*/
|
|
|
|
|
public function getArchiveQueryInfo() {
|
2018-01-29 14:25:49 +00:00
|
|
|
$commentQuery = $this->commentStore->getJoin( 'ar_comment' );
|
2017-09-12 17:12:29 +00:00
|
|
|
$actorQuery = $this->actorMigration->getJoin( 'ar_user' );
|
2017-08-27 15:29:18 +00:00
|
|
|
$ret = [
|
2017-09-12 17:12:29 +00:00
|
|
|
'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
|
2017-08-27 15:29:18 +00:00
|
|
|
'fields' => [
|
|
|
|
|
'ar_id',
|
|
|
|
|
'ar_page_id',
|
|
|
|
|
'ar_namespace',
|
|
|
|
|
'ar_title',
|
|
|
|
|
'ar_rev_id',
|
|
|
|
|
'ar_timestamp',
|
|
|
|
|
'ar_minor_edit',
|
|
|
|
|
'ar_deleted',
|
|
|
|
|
'ar_len',
|
|
|
|
|
'ar_parent_id',
|
|
|
|
|
'ar_sha1',
|
2017-09-12 17:12:29 +00:00
|
|
|
] + $commentQuery['fields'] + $actorQuery['fields'],
|
|
|
|
|
'joins' => $commentQuery['joins'] + $actorQuery['joins'],
|
2017-08-27 15:29:18 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Do a batched query for the sizes of a set of revisions.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::getParentLengths
|
|
|
|
|
*
|
2017-12-27 15:46:03 +00:00
|
|
|
* @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 ) {
|
2020-03-07 02:10:00 +00:00
|
|
|
$dbr = $this->getDBConnectionRef( DB_REPLICA );
|
2017-08-27 15:29:18 +00:00
|
|
|
$revLens = [];
|
|
|
|
|
if ( !$revIds ) {
|
|
|
|
|
return $revLens; // empty
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-07 02:10:00 +00:00
|
|
|
$res = $dbr->select(
|
2017-08-27 15:29:18 +00:00
|
|
|
'revision',
|
|
|
|
|
[ 'rev_id', 'rev_len' ],
|
|
|
|
|
[ 'rev_id' => $revIds ],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
$revLens[$row->rev_id] = intval( $row->rev_len );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $revLens;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-07 02:10:00 +00:00
|
|
|
/**
|
|
|
|
|
* Do a batched query for the sizes of a set of revisions.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::getParentLengths
|
|
|
|
|
*
|
|
|
|
|
* @deprecated since 1.35 use RevisionStore::getRevisionSizes instead.
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param int[] $revIds
|
|
|
|
|
* @return int[] associative array mapping revision IDs from $revIds to the nominal size
|
|
|
|
|
* of the corresponding revision.
|
|
|
|
|
*/
|
|
|
|
|
public function listRevisionSizes( IDatabase $db, array $revIds ) {
|
|
|
|
|
wfDeprecated( __METHOD__, '1.35' );
|
|
|
|
|
return $this->getRevisionSizes( $revIds );
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:29:18 +00:00
|
|
|
/**
|
2019-04-29 14:24:58 +00:00
|
|
|
* Implementation of getPreviousRevision and getNextRevision.
|
2018-11-23 12:30:35 +00:00
|
|
|
*
|
2017-08-27 15:29:18 +00:00
|
|
|
* @param RevisionRecord $rev
|
2019-04-29 14:24:58 +00:00
|
|
|
* @param int $flags
|
|
|
|
|
* @param string $dir 'next' or 'prev'
|
2017-08-27 15:29:18 +00:00
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
2019-04-29 14:24:58 +00:00
|
|
|
private function getRelativeRevision( RevisionRecord $rev, $flags, $dir ) {
|
|
|
|
|
$op = $dir === 'next' ? '>' : '<';
|
|
|
|
|
$sort = $dir === 'next' ? 'ASC' : 'DESC';
|
|
|
|
|
|
2018-11-23 12:30:35 +00:00
|
|
|
if ( !$rev->getId() || !$rev->getPageId() ) {
|
|
|
|
|
// revision is unsaved or otherwise incomplete
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $rev instanceof RevisionArchiveRecord ) {
|
|
|
|
|
// revision is deleted, so it's not part of the page history
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-29 14:24:58 +00:00
|
|
|
list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
|
2019-08-04 14:50:50 +00:00
|
|
|
$db = $this->getDBConnectionRef( $dbType, [ 'contributions' ] );
|
2019-04-29 14:24:58 +00:00
|
|
|
|
|
|
|
|
$ts = $this->getTimestampFromId( $rev->getId(), $flags );
|
|
|
|
|
if ( $ts === false ) {
|
|
|
|
|
// XXX Should this be moved into getTimestampFromId?
|
|
|
|
|
$ts = $db->selectField( 'archive', 'ar_timestamp',
|
|
|
|
|
[ 'ar_rev_id' => $rev->getId() ], __METHOD__ );
|
|
|
|
|
if ( $ts === false ) {
|
|
|
|
|
// XXX Is this reachable? How can we have a page id but no timestamp?
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-01-10 12:23:06 +00:00
|
|
|
}
|
2019-04-29 14:24:58 +00:00
|
|
|
$ts = $db->addQuotes( $db->timestamp( $ts ) );
|
|
|
|
|
|
|
|
|
|
$revId = $db->selectField( 'revision', 'rev_id',
|
|
|
|
|
[
|
|
|
|
|
'rev_page' => $rev->getPageId(),
|
|
|
|
|
"rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
|
|
|
|
|
],
|
|
|
|
|
__METHOD__,
|
|
|
|
|
[
|
2019-11-29 22:01:07 +00:00
|
|
|
'ORDER BY' => [ "rev_timestamp $sort", "rev_id $sort" ],
|
2019-04-29 14:24:58 +00:00
|
|
|
'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
|
|
|
|
|
]
|
|
|
|
|
);
|
2018-11-23 12:30:35 +00:00
|
|
|
|
2019-04-29 14:24:58 +00:00
|
|
|
if ( $revId === false ) {
|
2018-11-23 12:30:35 +00:00
|
|
|
return null;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
2018-11-23 12:30:35 +00:00
|
|
|
|
2019-04-29 14:24:58 +00:00
|
|
|
return $this->getRevisionById( intval( $revId ) );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-04-29 14:24:58 +00:00
|
|
|
* Get the revision before $rev in the page's history, if any.
|
|
|
|
|
* Will return null for the first revision but also for deleted or unsaved revisions.
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
2019-04-29 14:24:58 +00:00
|
|
|
* MCR migration note: this replaces Revision::getPrevious
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
2019-04-29 14:24:58 +00:00
|
|
|
* @see Title::getPreviousRevisionID
|
|
|
|
|
* @see PageArchive::getPreviousRevision
|
2018-11-23 12:30:35 +00:00
|
|
|
*
|
2017-08-27 15:29:18 +00:00
|
|
|
* @param RevisionRecord $rev
|
2019-04-29 14:24:58 +00:00
|
|
|
* @param int $flags (optional) $flags include:
|
|
|
|
|
* IDBAccessObject::READ_LATEST: Select the data from the master
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
2019-04-29 14:24:58 +00:00
|
|
|
public function getPreviousRevision( RevisionRecord $rev, $flags = 0 ) {
|
|
|
|
|
if ( $flags instanceof Title ) {
|
|
|
|
|
// Old calling convention, we don't use Title here anymore
|
|
|
|
|
wfDeprecated( __METHOD__ . ' with Title', '1.34' );
|
|
|
|
|
$flags = 0;
|
2018-11-23 12:30:35 +00:00
|
|
|
}
|
|
|
|
|
|
2019-04-29 14:24:58 +00:00
|
|
|
return $this->getRelativeRevision( $rev, $flags, 'prev' );
|
|
|
|
|
}
|
2018-11-23 12:30:35 +00:00
|
|
|
|
2019-04-29 14:24:58 +00:00
|
|
|
/**
|
|
|
|
|
* Get the revision after $rev in the page's history, if any.
|
|
|
|
|
* Will return null for the latest revision but also for deleted or unsaved revisions.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::getNext
|
|
|
|
|
*
|
|
|
|
|
* @see Title::getNextRevisionID
|
|
|
|
|
*
|
|
|
|
|
* @param RevisionRecord $rev
|
|
|
|
|
* @param int $flags (optional) $flags include:
|
|
|
|
|
* IDBAccessObject::READ_LATEST: Select the data from the master
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
public function getNextRevision( RevisionRecord $rev, $flags = 0 ) {
|
|
|
|
|
if ( $flags instanceof Title ) {
|
|
|
|
|
// Old calling convention, we don't use Title here anymore
|
|
|
|
|
wfDeprecated( __METHOD__ . ' with Title', '1.34' );
|
|
|
|
|
$flags = 0;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
2018-11-23 12:30:35 +00:00
|
|
|
|
2019-04-29 14:24:58 +00:00
|
|
|
return $this->getRelativeRevision( $rev, $flags, 'next' );
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get previous revision Id for this page_id
|
|
|
|
|
* This is used to populate rev_parent_id on save
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this corresponds to Revision::getPreviousRevisionId
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param RevisionRecord $rev
|
|
|
|
|
*
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
private function getPreviousRevisionId( IDatabase $db, RevisionRecord $rev ) {
|
2019-07-04 07:46:39 +00:00
|
|
|
$this->checkDatabaseDomain( $db );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
if ( $rev->getPageId() === null ) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
# Use page_latest if ID is not given
|
|
|
|
|
if ( !$rev->getId() ) {
|
|
|
|
|
$prevId = $db->selectField(
|
|
|
|
|
'page', 'page_latest',
|
|
|
|
|
[ 'page_id' => $rev->getPageId() ],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
$prevId = $db->selectField(
|
|
|
|
|
'revision', 'rev_id',
|
|
|
|
|
[ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
|
|
|
|
|
__METHOD__,
|
|
|
|
|
[ 'ORDER BY' => 'rev_id DESC' ]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return intval( $prevId );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
Don't require Title for getTimestampFromId
3e36ba655e3a added an option for passing a page ID to this method of
Revision, and e03787afd91c switched it to a Title and made it mandatory.
This behavior propagated to the method in RevisionStore. As far as I
can tell, the parameter does not help anything, but it can add a
database query to get the page ID if it's not cached, and impedes
conversion to LinkTarget. I can't figure out any reason to not
completely drop it. I've noted it as deprecated but still supported it
for now for compatibility -- I found one extension that does pass it.
(It's ignored, though, which theoretically would be a behavior change if
someone was passing a Title that didn't match the revision ID.)
While I was at it, I added the method to RevisionLookup, although it's
only used in later patches. Properly I should move that piece to a later
patch, but it didn't seem worth the effort.
I didn't change the Revision method, because the whole Revision class is
deprecated anyway.
Change-Id: I26ef5f2bf828f0f450633b7237d26d888e2c8773
2019-04-29 14:32:22 +00:00
|
|
|
* Get rev_timestamp from rev_id, without loading the rest of the row.
|
|
|
|
|
*
|
|
|
|
|
* Historically, there was an extra Title parameter that was passed before $id. This is no
|
|
|
|
|
* longer needed and is deprecated in 1.34.
|
2017-08-27 15:29:18 +00:00
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::getTimestampFromId
|
|
|
|
|
*
|
|
|
|
|
* @param int $id
|
|
|
|
|
* @param int $flags
|
|
|
|
|
* @return string|bool False if not found
|
|
|
|
|
*/
|
Don't require Title for getTimestampFromId
3e36ba655e3a added an option for passing a page ID to this method of
Revision, and e03787afd91c switched it to a Title and made it mandatory.
This behavior propagated to the method in RevisionStore. As far as I
can tell, the parameter does not help anything, but it can add a
database query to get the page ID if it's not cached, and impedes
conversion to LinkTarget. I can't figure out any reason to not
completely drop it. I've noted it as deprecated but still supported it
for now for compatibility -- I found one extension that does pass it.
(It's ignored, though, which theoretically would be a behavior change if
someone was passing a Title that didn't match the revision ID.)
While I was at it, I added the method to RevisionLookup, although it's
only used in later patches. Properly I should move that piece to a later
patch, but it didn't seem worth the effort.
I didn't change the Revision method, because the whole Revision class is
deprecated anyway.
Change-Id: I26ef5f2bf828f0f450633b7237d26d888e2c8773
2019-04-29 14:32:22 +00:00
|
|
|
public function getTimestampFromId( $id, $flags = 0 ) {
|
|
|
|
|
if ( $id instanceof Title ) {
|
|
|
|
|
// Old deprecated calling convention supported for backwards compatibility
|
|
|
|
|
$id = $flags;
|
|
|
|
|
$flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
|
|
|
|
|
}
|
2018-07-10 18:54:33 +00:00
|
|
|
$db = $this->getDBConnectionRefForQueryFlags( $flags );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
Don't require Title for getTimestampFromId
3e36ba655e3a added an option for passing a page ID to this method of
Revision, and e03787afd91c switched it to a Title and made it mandatory.
This behavior propagated to the method in RevisionStore. As far as I
can tell, the parameter does not help anything, but it can add a
database query to get the page ID if it's not cached, and impedes
conversion to LinkTarget. I can't figure out any reason to not
completely drop it. I've noted it as deprecated but still supported it
for now for compatibility -- I found one extension that does pass it.
(It's ignored, though, which theoretically would be a behavior change if
someone was passing a Title that didn't match the revision ID.)
While I was at it, I added the method to RevisionLookup, although it's
only used in later patches. Properly I should move that piece to a later
patch, but it didn't seem worth the effort.
I didn't change the Revision method, because the whole Revision class is
deprecated anyway.
Change-Id: I26ef5f2bf828f0f450633b7237d26d888e2c8773
2019-04-29 14:32:22 +00:00
|
|
|
$timestamp =
|
|
|
|
|
$db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $id ], __METHOD__ );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2020-04-02 20:00:24 +00:00
|
|
|
return ( $timestamp !== false ) ? MWTimestamp::convert( TS_MW, $timestamp ) : false;
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get count of revisions per page...not very efficient
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::countByPageId
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param int $id Page id
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
public function countRevisionsByPageId( IDatabase $db, $id ) {
|
2019-07-04 07:46:39 +00:00
|
|
|
$this->checkDatabaseDomain( $db );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
$row = $db->selectRow( 'revision',
|
|
|
|
|
[ 'revCount' => 'COUNT(*)' ],
|
|
|
|
|
[ 'rev_page' => $id ],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
if ( $row ) {
|
|
|
|
|
return intval( $row->revCount );
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get count of revisions per page...not very efficient
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::countByTitle
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param Title $title
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
public function countRevisionsByTitle( IDatabase $db, $title ) {
|
|
|
|
|
$id = $title->getArticleID();
|
|
|
|
|
if ( $id ) {
|
|
|
|
|
return $this->countRevisionsByPageId( $db, $id );
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if no edits were made by other users since
|
|
|
|
|
* the time a user started editing the page. Limit to
|
|
|
|
|
* 50 revisions for the sake of performance.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::userWasLastToEdit
|
|
|
|
|
*
|
|
|
|
|
* @deprecated since 1.31; Can possibly be removed, since the self-conflict suppression
|
|
|
|
|
* logic in EditPage that uses this seems conceptually dubious. Revision::userWasLastToEdit
|
|
|
|
|
* has been deprecated since 1.24.
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db The Database to perform the check on.
|
|
|
|
|
* @param int $pageId The ID of the page in question
|
|
|
|
|
* @param int $userId The ID of the user in question
|
|
|
|
|
* @param string $since Look at edits since this time
|
|
|
|
|
*
|
|
|
|
|
* @return bool True if the given user was the only one to edit since the given timestamp
|
|
|
|
|
*/
|
|
|
|
|
public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
|
2019-07-04 07:46:39 +00:00
|
|
|
$this->checkDatabaseDomain( $db );
|
2017-08-27 15:29:18 +00:00
|
|
|
|
|
|
|
|
if ( !$userId ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-29 15:54:02 +00:00
|
|
|
$revQuery = $this->getQueryInfo();
|
2017-08-27 15:29:18 +00:00
|
|
|
$res = $db->select(
|
2017-09-12 17:12:29 +00:00
|
|
|
$revQuery['tables'],
|
|
|
|
|
[
|
|
|
|
|
'rev_user' => $revQuery['fields']['rev_user'],
|
|
|
|
|
],
|
2017-08-27 15:29:18 +00:00
|
|
|
[
|
|
|
|
|
'rev_page' => $pageId,
|
|
|
|
|
'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
|
|
|
|
|
],
|
|
|
|
|
__METHOD__,
|
2017-09-12 17:12:29 +00:00
|
|
|
[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
|
|
|
|
|
$revQuery['joins']
|
2017-08-27 15:29:18 +00:00
|
|
|
);
|
|
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
if ( $row->rev_user != $userId ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load a revision based on a known page ID and current revision ID from the DB
|
|
|
|
|
*
|
|
|
|
|
* This method allows for the use of caching, though accessing anything that normally
|
|
|
|
|
* requires permission checks (aside from the text) will trigger a small DB lookup.
|
|
|
|
|
*
|
|
|
|
|
* MCR migration note: this replaces Revision::newKnownCurrent
|
|
|
|
|
*
|
|
|
|
|
* @param Title $title the associated page title
|
|
|
|
|
* @param int $revId current revision of this page. Defaults to $title->getLatestRevID().
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionRecord|bool Returns false if missing
|
|
|
|
|
*/
|
2019-11-21 15:21:41 +00:00
|
|
|
public function getKnownCurrentRevision( Title $title, $revId = 0 ) {
|
2017-08-27 15:29:18 +00:00
|
|
|
$db = $this->getDBConnectionRef( DB_REPLICA );
|
|
|
|
|
|
2020-04-09 12:06:22 +00:00
|
|
|
$revIdPassed = $revId;
|
2017-08-27 15:29:18 +00:00
|
|
|
$pageId = $title->getArticleID();
|
|
|
|
|
|
|
|
|
|
if ( !$pageId ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$revId ) {
|
|
|
|
|
$revId = $title->getLatestRevID();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$revId ) {
|
|
|
|
|
wfWarn(
|
|
|
|
|
'No latest revision known for page ' . $title->getPrefixedDBkey()
|
|
|
|
|
. ' even though it exists with page ID ' . $pageId
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-27 21:26:17 +00:00
|
|
|
// Load the row from cache if possible. If not possible, populate the cache.
|
|
|
|
|
// As a minor optimization, remember if this was a cache hit or miss.
|
|
|
|
|
// We can sometimes avoid a database query later if this is a cache miss.
|
|
|
|
|
$fromCache = true;
|
2017-08-27 15:29:18 +00:00
|
|
|
$row = $this->cache->getWithSetCallback(
|
|
|
|
|
// Page/rev IDs passed in from DB to reflect history merges
|
2018-01-29 15:54:02 +00:00
|
|
|
$this->getRevisionRowCacheKey( $db, $pageId, $revId ),
|
2017-08-27 15:29:18 +00:00
|
|
|
WANObjectCache::TTL_WEEK,
|
2019-02-27 21:26:17 +00:00
|
|
|
function ( $curValue, &$ttl, array &$setOpts ) use (
|
2020-02-28 22:06:46 +00:00
|
|
|
$db, $revId, &$fromCache
|
2019-02-27 21:26:17 +00:00
|
|
|
) {
|
2017-08-27 15:29:18 +00:00
|
|
|
$setOpts += Database::getCacheSetOptions( $db );
|
2019-02-27 21:26:17 +00:00
|
|
|
$row = $this->fetchRevisionRowFromConds( $db, [ 'rev_id' => intval( $revId ) ] );
|
|
|
|
|
if ( $row ) {
|
|
|
|
|
$fromCache = false;
|
|
|
|
|
}
|
|
|
|
|
return $row; // don't cache negatives
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2019-02-27 21:26:17 +00:00
|
|
|
// Reflect revision deletion and user renames.
|
2017-08-27 15:29:18 +00:00
|
|
|
if ( $row ) {
|
2020-04-09 12:06:22 +00:00
|
|
|
$this->ensureRevisionRowMatchesTitle( $row, $title, [
|
|
|
|
|
'from_cache_flag' => $fromCache,
|
|
|
|
|
'page_id_initial' => $pageId,
|
|
|
|
|
'rev_id_used' => $revId,
|
|
|
|
|
'rev_id_requested' => $revIdPassed,
|
|
|
|
|
] );
|
|
|
|
|
|
2019-02-27 21:26:17 +00:00
|
|
|
return $this->newRevisionFromRow( $row, 0, $title, $fromCache );
|
2017-08-27 15:29:18 +00:00
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-07 07:07:42 +00:00
|
|
|
/**
|
|
|
|
|
* Get the first revision of a given page.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.35
|
|
|
|
|
* @param LinkTarget $title
|
|
|
|
|
* @param int $flags
|
|
|
|
|
* @return RevisionRecord|null
|
|
|
|
|
*/
|
|
|
|
|
public function getFirstRevision(
|
|
|
|
|
LinkTarget $title,
|
|
|
|
|
int $flags = IDBAccessObject::READ_NORMAL
|
|
|
|
|
): ?RevisionRecord {
|
|
|
|
|
$titleObj = Title::newFromLinkTarget( $title ); //TODO: eventually we shouldn't need a title
|
|
|
|
|
return $this->newRevisionFromConds(
|
|
|
|
|
[
|
|
|
|
|
'page_namespace' => $title->getNamespace(),
|
|
|
|
|
'page_title' => $title->getDBkey()
|
|
|
|
|
],
|
|
|
|
|
$flags,
|
|
|
|
|
$titleObj,
|
|
|
|
|
[
|
|
|
|
|
'ORDER BY' => [ 'rev_timestamp ASC', 'rev_id ASC' ],
|
|
|
|
|
'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-29 15:54:02 +00:00
|
|
|
/**
|
|
|
|
|
* Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] )
|
|
|
|
|
* Caching rows without 'page' or 'user' could lead to issues.
|
|
|
|
|
* If the format of the rows returned by the query provided by getQueryInfo changes the
|
|
|
|
|
* cache key should be updated to avoid conflicts.
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $db
|
|
|
|
|
* @param int $pageId
|
|
|
|
|
* @param int $revId
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
|
|
|
|
|
return $this->cache->makeGlobalKey(
|
|
|
|
|
self::ROW_CACHE_KEY,
|
|
|
|
|
$db->getDomainID(),
|
|
|
|
|
$pageId,
|
|
|
|
|
$revId
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-16 20:39:02 +00:00
|
|
|
/**
|
|
|
|
|
* Asserts that if revision is provided, it's saved and belongs to the page with provided pageId.
|
|
|
|
|
* @param string $paramName
|
|
|
|
|
* @param int $pageId
|
|
|
|
|
* @param RevisionRecord|null $rev
|
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
|
*/
|
|
|
|
|
private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev = null ) {
|
|
|
|
|
if ( $rev ) {
|
|
|
|
|
if ( $rev->getId() === null ) {
|
|
|
|
|
throw new InvalidArgumentException( "Unsaved {$paramName} revision passed" );
|
|
|
|
|
}
|
|
|
|
|
if ( $rev->getPageId() !== $pageId ) {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"Revision {$rev->getId()} doesn't belong to page {$pageId}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-24 19:08:33 +00:00
|
|
|
/**
|
|
|
|
|
* Converts revision limits to query conditions.
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase $dbr
|
|
|
|
|
* @param RevisionRecord|null $old Old revision.
|
|
|
|
|
* @param RevisionRecord|null $new New revision.
|
|
|
|
|
* @param array $options Single option, or an array of options:
|
|
|
|
|
* 'include_old' Include $old in the range; $new is excluded.
|
|
|
|
|
* 'include_new' Include $new in the range; $old is excluded.
|
|
|
|
|
* 'include_both' Include both $old and $new in the range.
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
private function getRevisionLimitConditions(
|
|
|
|
|
IDatabase $dbr,
|
|
|
|
|
RevisionRecord $old = null,
|
|
|
|
|
RevisionRecord $new = null,
|
|
|
|
|
$options = []
|
|
|
|
|
) {
|
|
|
|
|
$options = (array)$options;
|
|
|
|
|
$oldCmp = '>';
|
|
|
|
|
$newCmp = '<';
|
|
|
|
|
if ( in_array( 'include_old', $options ) ) {
|
|
|
|
|
$oldCmp = '>=';
|
|
|
|
|
}
|
|
|
|
|
if ( in_array( 'include_new', $options ) ) {
|
|
|
|
|
$newCmp = '<=';
|
|
|
|
|
}
|
|
|
|
|
if ( in_array( 'include_both', $options ) ) {
|
|
|
|
|
$oldCmp = '>=';
|
|
|
|
|
$newCmp = '<=';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$conds = [];
|
|
|
|
|
if ( $old ) {
|
|
|
|
|
$oldTs = $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) );
|
|
|
|
|
$conds[] = "(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId()}) " .
|
|
|
|
|
"OR rev_timestamp > {$oldTs}";
|
|
|
|
|
}
|
|
|
|
|
if ( $new ) {
|
|
|
|
|
$newTs = $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) );
|
|
|
|
|
$conds[] = "(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId()}) " .
|
|
|
|
|
"OR rev_timestamp < {$newTs}";
|
|
|
|
|
}
|
|
|
|
|
return $conds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the authors between the given revisions or revisions.
|
|
|
|
|
* Used for diffs and other things that really need it.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.35
|
|
|
|
|
*
|
|
|
|
|
* @param int $pageId The id of the page
|
|
|
|
|
* @param RevisionRecord|null $old Old revision.
|
2020-03-30 20:20:27 +00:00
|
|
|
* If null is provided, count starting from the first revision (inclusive).
|
2019-10-24 19:08:33 +00:00
|
|
|
* @param RevisionRecord|null $new New revision.
|
|
|
|
|
* If null is provided, count until the last revision (inclusive).
|
|
|
|
|
* @param User|null $user the user who's access rights to apply
|
|
|
|
|
* @param int|null $max Limit of Revisions to count, will be incremented to detect truncations.
|
|
|
|
|
* @param string|array $options Single option, or an array of options:
|
|
|
|
|
* 'include_old' Include $old in the range; $new is excluded.
|
|
|
|
|
* 'include_new' Include $new in the range; $old is excluded.
|
|
|
|
|
* 'include_both' Include both $old and $new in the range.
|
|
|
|
|
* @throws InvalidArgumentException in case either revision is unsaved or
|
2020-03-30 20:20:27 +00:00
|
|
|
* the revisions do not belong to the same page or unknown option is passed.
|
2019-10-24 19:08:33 +00:00
|
|
|
* @return UserIdentity[] Names of revision authors in the range
|
|
|
|
|
*/
|
|
|
|
|
public function getAuthorsBetween(
|
|
|
|
|
$pageId,
|
|
|
|
|
RevisionRecord $old = null,
|
|
|
|
|
RevisionRecord $new = null,
|
|
|
|
|
User $user = null,
|
|
|
|
|
$max = null,
|
|
|
|
|
$options = []
|
|
|
|
|
) {
|
|
|
|
|
$this->assertRevisionParameter( 'old', $pageId, $old );
|
|
|
|
|
$this->assertRevisionParameter( 'new', $pageId, $new );
|
|
|
|
|
$options = (array)$options;
|
|
|
|
|
|
|
|
|
|
// No DB query needed if old and new are the same revision.
|
|
|
|
|
// Can't check for consecutive revisions with 'getParentId' for a similar
|
|
|
|
|
// optimization as edge cases exist when there are revisions between
|
|
|
|
|
//a revision and it's parent. See T185167 for more details.
|
|
|
|
|
if ( $old && $new && $new->getId() === $old->getId() ) {
|
|
|
|
|
if ( empty( $options ) ) {
|
|
|
|
|
return [];
|
|
|
|
|
} else {
|
2019-11-21 15:21:41 +00:00
|
|
|
return $user ? [ $new->getUser( RevisionRecord::FOR_PUBLIC, $user ) ] : [ $new->getUser() ];
|
2019-10-24 19:08:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dbr = $this->getDBConnectionRef( DB_REPLICA );
|
|
|
|
|
$conds = array_merge(
|
2019-11-21 15:21:41 +00:00
|
|
|
[
|
|
|
|
|
'rev_page' => $pageId,
|
|
|
|
|
$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) . " = 0"
|
|
|
|
|
],
|
2019-10-24 19:08:33 +00:00
|
|
|
$this->getRevisionLimitConditions( $dbr, $old, $new, $options )
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$queryOpts = [ 'DISTINCT' ];
|
|
|
|
|
if ( $max !== null ) {
|
|
|
|
|
$queryOpts['LIMIT'] = $max + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$actorQuery = $this->actorMigration->getJoin( 'rev_user' );
|
|
|
|
|
return array_map( function ( $row ) {
|
|
|
|
|
return new UserIdentityValue( (int)$row->rev_user, $row->rev_user_text, (int)$row->rev_actor );
|
|
|
|
|
}, iterator_to_array( $dbr->select(
|
|
|
|
|
array_merge( [ 'revision' ], $actorQuery['tables'] ),
|
|
|
|
|
$actorQuery['fields'],
|
|
|
|
|
$conds, __METHOD__,
|
|
|
|
|
$queryOpts,
|
|
|
|
|
$actorQuery['joins']
|
|
|
|
|
) ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the number of authors between the given revisions.
|
|
|
|
|
* Used for diffs and other things that really need it.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.35
|
|
|
|
|
*
|
|
|
|
|
* @param int $pageId The id of the page
|
|
|
|
|
* @param RevisionRecord|null $old Old revision .
|
2020-03-30 20:20:27 +00:00
|
|
|
* If null is provided, count starting from the first revision (inclusive).
|
2019-10-24 19:08:33 +00:00
|
|
|
* @param RevisionRecord|null $new New revision.
|
|
|
|
|
* If null is provided, count until the last revision (inclusive).
|
|
|
|
|
* @param User|null $user the user who's access rights to apply
|
|
|
|
|
* @param int|null $max Limit of Revisions to count, will be incremented to detect truncations.
|
|
|
|
|
* @param string|array $options Single option, or an array of options:
|
|
|
|
|
* 'include_old' Include $old in the range; $new is excluded.
|
|
|
|
|
* 'include_new' Include $new in the range; $old is excluded.
|
|
|
|
|
* 'include_both' Include both $old and $new in the range.
|
|
|
|
|
* @throws InvalidArgumentException in case either revision is unsaved or
|
2020-03-30 20:20:27 +00:00
|
|
|
* the revisions do not belong to the same page or unknown option is passed.
|
2019-10-24 19:08:33 +00:00
|
|
|
* @return int Number of revisions authors in the range.
|
|
|
|
|
*/
|
|
|
|
|
public function countAuthorsBetween(
|
|
|
|
|
$pageId,
|
|
|
|
|
RevisionRecord $old = null,
|
|
|
|
|
RevisionRecord $new = null,
|
|
|
|
|
User $user = null,
|
|
|
|
|
$max = null,
|
|
|
|
|
$options = []
|
|
|
|
|
) {
|
|
|
|
|
// TODO: Implement with a separate query to avoid cost of selecting unneeded fields
|
|
|
|
|
// and creation of UserIdentity stuff.
|
|
|
|
|
return count( $this->getAuthorsBetween( $pageId, $old, $new, $user, $max, $options ) );
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-16 20:10:58 +00:00
|
|
|
/**
|
|
|
|
|
* Get the number of revisions between the given revisions.
|
|
|
|
|
* Used for diffs and other things that really need it.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.35
|
|
|
|
|
*
|
2019-10-16 20:39:02 +00:00
|
|
|
* @param int $pageId The id of the page
|
2019-10-24 19:08:33 +00:00
|
|
|
* @param RevisionRecord|null $old Old revision.
|
2020-03-30 20:20:27 +00:00
|
|
|
* If null is provided, count starting from the first revision (inclusive).
|
2019-10-24 19:08:33 +00:00
|
|
|
* @param RevisionRecord|null $new New revision.
|
2019-10-16 20:39:02 +00:00
|
|
|
* If null is provided, count until the last revision (inclusive).
|
2019-10-16 20:10:58 +00:00
|
|
|
* @param int|null $max Limit of Revisions to count, will be incremented to detect truncations.
|
2019-10-24 19:08:33 +00:00
|
|
|
* @param string|array $options Single option, or an array of options:
|
|
|
|
|
* 'include_old' Include $old in the range; $new is excluded.
|
|
|
|
|
* 'include_new' Include $new in the range; $old is excluded.
|
|
|
|
|
* 'include_both' Include both $old and $new in the range.
|
2019-10-16 20:10:58 +00:00
|
|
|
* @throws InvalidArgumentException in case either revision is unsaved or
|
2020-03-30 20:20:27 +00:00
|
|
|
* the revisions do not belong to the same page.
|
2019-10-16 20:10:58 +00:00
|
|
|
* @return int Number of revisions between these revisions.
|
|
|
|
|
*/
|
2019-10-16 20:39:02 +00:00
|
|
|
public function countRevisionsBetween(
|
|
|
|
|
$pageId,
|
|
|
|
|
RevisionRecord $old = null,
|
|
|
|
|
RevisionRecord $new = null,
|
2019-10-24 19:08:33 +00:00
|
|
|
$max = null,
|
|
|
|
|
$options = []
|
2019-10-16 20:39:02 +00:00
|
|
|
) {
|
|
|
|
|
$this->assertRevisionParameter( 'old', $pageId, $old );
|
|
|
|
|
$this->assertRevisionParameter( 'new', $pageId, $new );
|
|
|
|
|
|
|
|
|
|
// No DB query needed if old and new are the same revision.
|
|
|
|
|
// Can't check for consecutive revisions with 'getParentId' for a similar
|
|
|
|
|
// optimization as edge cases exist when there are revisions between
|
|
|
|
|
//a revision and it's parent. See T185167 for more details.
|
|
|
|
|
if ( $old && $new && $new->getId() === $old->getId() ) {
|
|
|
|
|
return 0;
|
2019-10-16 20:10:58 +00:00
|
|
|
}
|
2017-08-27 15:29:18 +00:00
|
|
|
|
2019-10-16 20:10:58 +00:00
|
|
|
$dbr = $this->getDBConnectionRef( DB_REPLICA );
|
2019-10-24 19:08:33 +00:00
|
|
|
$conds = array_merge(
|
2019-11-21 15:21:41 +00:00
|
|
|
[
|
|
|
|
|
'rev_page' => $pageId,
|
|
|
|
|
$dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . " = 0"
|
|
|
|
|
],
|
2019-10-24 19:08:33 +00:00
|
|
|
$this->getRevisionLimitConditions( $dbr, $old, $new, $options )
|
|
|
|
|
);
|
2019-10-16 20:10:58 +00:00
|
|
|
if ( $max !== null ) {
|
|
|
|
|
return $dbr->selectRowCount( 'revision', '1',
|
|
|
|
|
$conds,
|
|
|
|
|
__METHOD__,
|
|
|
|
|
[ 'LIMIT' => $max + 1 ] // extra to detect truncation
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
|
2017-08-27 15:29:18 +00:00
|
|
|
}
|
2018-09-20 17:29:04 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retain the old class name for backwards compatibility.
|
|
|
|
|
* @deprecated since 1.32
|
|
|
|
|
*/
|
|
|
|
|
class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
|