deprecated. Use CommentStore::insert() instead.
* Language::setCode is deprecated as public function. Use Language::factory
to create a new Language object with a different language code.
+* Several classes have been moved from the MediaWiki\Storage\ namespace to the
+ MediaWiki\Revision\ namespace. The old class names are aliased for
+ compatibility, but are deprecated. Classes are IncompleteRevisionException,
+ MutableRevisionRecord, MutableRevisionSlots, RevisionAccessException,
+ RevisionArchiveRecord, RevisionFactory, RevisionLookup, RevisionRecord,
+ RevisionSlots, RevisionStore, RevisionStoreRecord, SlotRecord, and
+ SuppressedDataException.
=== Other changes in 1.32 ===
* (T198811) The following tables have had their UNIQUE indexes turned into
'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
'MediaWiki\\OutputHandler' => __DIR__ . '/includes/OutputHandler.php',
'MediaWiki\\ProcOpenError' => __DIR__ . '/includes/exception/ProcOpenError.php',
- 'MediaWiki\\Revision\\RenderedRevision' => __DIR__ . '/includes/Revision/RenderedRevision.php',
- 'MediaWiki\\Revision\\RevisionRenderer' => __DIR__ . '/includes/Revision/RevisionRenderer.php',
- 'MediaWiki\\Revision\\SlotRenderingProvider' => __DIR__ . '/includes/Revision/SlotRenderingProvider.php',
'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php',
'MediaWiki\\ShellDisabledError' => __DIR__ . '/includes/exception/ShellDisabledError.php',
'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
'MediaWiki\\Special\\SpecialPageFactory' => __DIR__ . '/includes/specialpage/SpecialPageFactory.php',
+ 'MediaWiki\\Storage\\IncompleteRevisionException' => __DIR__ . '/includes/Revision/IncompleteRevisionException.php',
+ 'MediaWiki\\Storage\\MutableRevisionRecord' => __DIR__ . '/includes/Revision/MutableRevisionRecord.php',
+ 'MediaWiki\\Storage\\MutableRevisionSlots' => __DIR__ . '/includes/Revision/MutableRevisionSlots.php',
+ 'MediaWiki\\Storage\\RevisionAccessException' => __DIR__ . '/includes/Revision/RevisionAccessException.php',
+ 'MediaWiki\\Storage\\RevisionArchiveRecord' => __DIR__ . '/includes/Revision/RevisionArchiveRecord.php',
+ 'MediaWiki\\Storage\\RevisionFactory' => __DIR__ . '/includes/Revision/RevisionFactory.php',
+ 'MediaWiki\\Storage\\RevisionLookup' => __DIR__ . '/includes/Revision/RevisionLookup.php',
+ 'MediaWiki\\Storage\\RevisionRecord' => __DIR__ . '/includes/Revision/RevisionRecord.php',
+ 'MediaWiki\\Storage\\RevisionSlots' => __DIR__ . '/includes/Revision/RevisionSlots.php',
+ 'MediaWiki\\Storage\\RevisionStore' => __DIR__ . '/includes/Revision/RevisionStore.php',
+ 'MediaWiki\\Storage\\RevisionStoreRecord' => __DIR__ . '/includes/Revision/RevisionStoreRecord.php',
+ 'MediaWiki\\Storage\\SlotRecord' => __DIR__ . '/includes/Revision/SlotRecord.php',
+ 'MediaWiki\\Storage\\SuppressedDataException' => __DIR__ . '/includes/Revision/SuppressedDataException.php',
'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php',
'MediaWiki\\User\\UserIdentityValue' => __DIR__ . '/includes/user/UserIdentityValue.php',
'MediaWiki\\Widget\\CheckMatrixWidget' => __DIR__ . '/includes/widget/CheckMatrixWidget.php',
'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
+ 'MediaWiki\\Revision\\' => __DIR__ . '/Revision/',
'MediaWiki\\Services\\' => __DIR__ . '/services/',
'MediaWiki\\Session\\' => __DIR__ . '/session/',
'MediaWiki\\Shell\\' => __DIR__ . '/shell/',
use MediaWiki\Storage\BlobStoreFactory;
use MediaWiki\Storage\NameTableStore;
use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\RevisionFactory;
-use MediaWiki\Storage\RevisionLookup;
-use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Revision\RevisionFactory;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionStore;
use OldRevisionImporter;
-use MediaWiki\Storage\RevisionStoreFactory;
+use MediaWiki\Revision\RevisionStoreFactory;
use UploadRevisionImporter;
use Wikimedia\Rdbms\LBFactory;
use LinkCache;
* @file
*/
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionFactory;
-use MediaWiki\Storage\RevisionLookup;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionFactory;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\SqlBlobStore;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
--- /dev/null
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+/**
+ * Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\IncompleteRevisionException
+ */
+class IncompleteRevisionException extends RevisionAccessException {
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( IncompleteRevisionException::class, 'MediaWiki\Storage\IncompleteRevisionException' );
--- /dev/null
+<?php
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ * Provides setters for all fields.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionRecord
+ */
+class MutableRevisionRecord extends RevisionRecord {
+
+ /**
+ * Returns an incomplete MutableRevisionRecord which uses $parent as its
+ * parent revision, and inherits all slots form it. If saved unchanged,
+ * the new revision will act as a null-revision.
+ *
+ * @param RevisionRecord $parent
+ *
+ * @return MutableRevisionRecord
+ */
+ public static function newFromParentRevision( RevisionRecord $parent ) {
+ // TODO: ideally, we wouldn't need a Title here
+ $title = Title::newFromLinkTarget( $parent->getPageAsLinkTarget() );
+ $rev = new MutableRevisionRecord( $title, $parent->getWikiId() );
+
+ foreach ( $parent->getSlotRoles() as $role ) {
+ $slot = $parent->getSlot( $role, self::RAW );
+ $rev->inheritSlot( $slot );
+ }
+
+ $rev->setPageId( $parent->getPageId() );
+ $rev->setParentId( $parent->getId() );
+
+ return $rev;
+ }
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ *
+ * @throws MWException
+ */
+ function __construct( Title $title, $wikiId = false ) {
+ $slots = new MutableRevisionSlots();
+
+ parent::__construct( $title, $slots, $wikiId );
+
+ $this->mSlots = $slots; // redundant, but nice for static analysis
+ }
+
+ /**
+ * @param int $parentId
+ */
+ public function setParentId( $parentId ) {
+ Assert::parameterType( 'integer', $parentId, '$parentId' );
+
+ $this->mParentId = $parentId;
+ }
+
+ /**
+ * Sets the given slot. If a slot with the same role is already present in the revision,
+ * it is replaced.
+ *
+ * @note This can only be used with a fresh "unattached" SlotRecord. Calling code that has a
+ * SlotRecord from another revision should use inheritSlot(). Calling code that has access to
+ * a Content object can use setContent().
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param SlotRecord $slot
+ */
+ public function setSlot( SlotRecord $slot ) {
+ if ( $slot->hasRevision() && $slot->getRevision() !== $this->getId() ) {
+ throw new InvalidArgumentException(
+ 'The given slot must be an unsaved, unattached one. '
+ . 'This slot is already attached to revision ' . $slot->getRevision() . '. '
+ . 'Use inheritSlot() instead to preserve a slot from a previous revision.'
+ );
+ }
+
+ $this->mSlots->setSlot( $slot );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * "Inherits" the given slot's content.
+ *
+ * If a slot with the same role is already present in the revision, it is replaced.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param SlotRecord $parentSlot
+ */
+ public function inheritSlot( SlotRecord $parentSlot ) {
+ $this->mSlots->inheritSlot( $parentSlot );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * Sets the content for the slot with the given role.
+ *
+ * If a slot with the same role is already present in the revision, it is replaced.
+ * Calling code that has access to a SlotRecord can use inheritSlot() instead.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param string $role
+ * @param Content $content
+ */
+ public function setContent( $role, Content $content ) {
+ $this->mSlots->setContent( $role, $content );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * Removes the slot with the given role from this revision.
+ * This effectively ends the "stream" with that role on the revision's page.
+ * Future revisions will no longer inherit this slot, unless it is added back explicitly.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param string $role
+ */
+ public function removeSlot( $role ) {
+ $this->mSlots->removeSlot( $role );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * Applies the given update to the slots of this revision.
+ *
+ * @param RevisionSlotsUpdate $update
+ */
+ public function applyUpdate( RevisionSlotsUpdate $update ) {
+ $update->apply( $this->mSlots );
+ }
+
+ /**
+ * @param CommentStoreComment $comment
+ */
+ public function setComment( CommentStoreComment $comment ) {
+ $this->mComment = $comment;
+ }
+
+ /**
+ * Set revision hash, for optimization. Prevents getSha1() from re-calculating the hash.
+ *
+ * @note This should only be used if the calling code is sure that the given hash is correct
+ * for the revision's content, and there is no chance of the content being manipulated
+ * later. When in doubt, this method should not be called.
+ *
+ * @param string $sha1 SHA1 hash as a base36 string.
+ */
+ public function setSha1( $sha1 ) {
+ Assert::parameterType( 'string', $sha1, '$sha1' );
+
+ $this->mSha1 = $sha1;
+ }
+
+ /**
+ * Set nominal revision size, for optimization. Prevents getSize() from re-calculating the size.
+ *
+ * @note This should only be used if the calling code is sure that the given size is correct
+ * for the revision's content, and there is no chance of the content being manipulated
+ * later. When in doubt, this method should not be called.
+ *
+ * @param int $size nominal size in bogo-bytes
+ */
+ public function setSize( $size ) {
+ Assert::parameterType( 'integer', $size, '$size' );
+
+ $this->mSize = $size;
+ }
+
+ /**
+ * @param int $visibility
+ */
+ public function setVisibility( $visibility ) {
+ Assert::parameterType( 'integer', $visibility, '$visibility' );
+
+ $this->mDeleted = $visibility;
+ }
+
+ /**
+ * @param string $timestamp A timestamp understood by wfTimestamp
+ */
+ public function setTimestamp( $timestamp ) {
+ Assert::parameterType( 'string', $timestamp, '$timestamp' );
+
+ $this->mTimestamp = wfTimestamp( TS_MW, $timestamp );
+ }
+
+ /**
+ * @param bool $minorEdit
+ */
+ public function setMinorEdit( $minorEdit ) {
+ Assert::parameterType( 'boolean', $minorEdit, '$minorEdit' );
+
+ $this->mMinorEdit = $minorEdit;
+ }
+
+ /**
+ * Set the revision ID.
+ *
+ * MCR migration note: this replaces Revision::setId()
+ *
+ * @warning Use this with care, especially when preparing a revision for insertion
+ * into the database! The revision ID should only be fixed in special cases
+ * like preserving the original ID when restoring a revision.
+ *
+ * @param int $id
+ */
+ public function setId( $id ) {
+ Assert::parameterType( 'integer', $id, '$id' );
+
+ $this->mId = $id;
+ }
+
+ /**
+ * Sets the user identity associated with the revision
+ *
+ * @param UserIdentity $user
+ */
+ public function setUser( UserIdentity $user ) {
+ $this->mUser = $user;
+ }
+
+ /**
+ * @param int $pageId
+ */
+ public function setPageId( $pageId ) {
+ Assert::parameterType( 'integer', $pageId, '$pageId' );
+
+ if ( $this->mTitle->exists() && $pageId !== $this->mTitle->getArticleID() ) {
+ throw new InvalidArgumentException(
+ 'The given Title does not belong to page ID ' . $this->mPageId
+ );
+ }
+
+ $this->mPageId = $pageId;
+ }
+
+ /**
+ * Returns the nominal size of this revision.
+ *
+ * MCR migration note: this replaces Revision::getSize
+ *
+ * @return int The nominal size, may be computed on the fly if not yet known.
+ */
+ public function getSize() {
+ // If not known, re-calculate and remember. Will be reset when slots change.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * Returns the base36 sha1 of this revision.
+ *
+ * MCR migration note: this replaces Revision::getSha1
+ *
+ * @return string The revision hash, may be computed on the fly if not yet known.
+ */
+ public function getSha1() {
+ // If not known, re-calculate and remember. Will be reset when slots change.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * Returns the slots defined for this revision as a MutableRevisionSlots instance,
+ * which can be modified to defined the slots for this revision.
+ *
+ * @return MutableRevisionSlots
+ */
+ public function getSlots() {
+ // Overwritten just guarantee the more narrow return type.
+ return parent::getSlots();
+ }
+
+ /**
+ * Invalidate cached aggregate values such as hash and size.
+ */
+ private function resetAggregateValues() {
+ $this->mSize = null;
+ $this->mSha1 = null;
+ }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( MutableRevisionRecord::class, 'MediaWiki\Storage\MutableRevisionRecord' );
--- /dev/null
+<?php
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use Content;
+
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\MutableRevisionSlots
+ */
+class MutableRevisionSlots extends RevisionSlots {
+
+ /**
+ * Constructs a MutableRevisionSlots that inherits from the given
+ * list of slots.
+ *
+ * @param SlotRecord[] $slots
+ *
+ * @return MutableRevisionSlots
+ */
+ public static function newFromParentRevisionSlots( array $slots ) {
+ $inherited = [];
+ foreach ( $slots as $slot ) {
+ $role = $slot->getRole();
+ $inherited[$role] = SlotRecord::newInherited( $slot );
+ }
+
+ return new MutableRevisionSlots( $inherited );
+ }
+
+ /**
+ * @param SlotRecord[] $slots An array of SlotRecords.
+ */
+ public function __construct( array $slots = [] ) {
+ parent::__construct( $slots );
+ }
+
+ /**
+ * Sets the given slot.
+ * If a slot with the same role is already present, it is replaced.
+ *
+ * @param SlotRecord $slot
+ */
+ public function setSlot( SlotRecord $slot ) {
+ if ( !is_array( $this->slots ) ) {
+ $this->getSlots(); // initialize $this->slots
+ }
+
+ $role = $slot->getRole();
+ $this->slots[$role] = $slot;
+ }
+
+ /**
+ * Sets the given slot to an inherited version of $slot.
+ * If a slot with the same role is already present, it is replaced.
+ *
+ * @param SlotRecord $slot
+ */
+ public function inheritSlot( SlotRecord $slot ) {
+ $this->setSlot( SlotRecord::newInherited( $slot ) );
+ }
+
+ /**
+ * Sets the content for the slot with the given role.
+ * If a slot with the same role is already present, it is replaced.
+ *
+ * @param string $role
+ * @param Content $content
+ */
+ public function setContent( $role, Content $content ) {
+ $slot = SlotRecord::newUnsaved( $role, $content );
+ $this->setSlot( $slot );
+ }
+
+ /**
+ * Remove the slot for the given role, discontinue the corresponding stream.
+ *
+ * @param string $role
+ */
+ public function removeSlot( $role ) {
+ if ( !is_array( $this->slots ) ) {
+ $this->getSlots(); // initialize $this->slots
+ }
+
+ unset( $this->slots[$role] );
+ }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( MutableRevisionSlots::class, 'MediaWiki\Storage\MutableRevisionSlots' );
use InvalidArgumentException;
use LogicException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SuppressedDataException;
use ParserOptions;
use ParserOutput;
use Psr\Log\LoggerInterface;
--- /dev/null
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionAccessException
+ */
+class RevisionAccessException extends RuntimeException {
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionAccessException::class, 'MediaWiki\Storage\RevisionAccessException' );
--- /dev/null
+<?php
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use MediaWiki\User\UserIdentity;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ * Most getters on RevisionArchiveRecord will never return null. However, getId() and
+ * getParentId() may indeed return null if this information was not stored when the archive entry
+ * was created.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionArchiveRecord
+ */
+class RevisionArchiveRecord extends RevisionRecord {
+
+ /**
+ * @var int
+ */
+ protected $mArchiveId;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
+ * a query that yields the required fields.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ */
+ function __construct(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ parent::__construct( $title, $slots, $wikiId );
+ Assert::parameterType( 'object', $row, '$row' );
+
+ $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
+ Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
+
+ $this->mArchiveId = intval( $row->ar_id );
+
+ // NOTE: ar_page_id may be different from $this->mTitle->getArticleID() in some cases,
+ // notably when a partially restored page has been moved, and a new page has been created
+ // with the same title. Archive rows for that title will then have the wrong page id.
+ $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $title->getArticleID();
+
+ // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null
+ // indicates that the parent revision is unknown. As per MW 1.31, the database schema
+ // allows ar_parent_id to be NULL.
+ $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null;
+ $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null;
+ $this->mComment = $comment;
+ $this->mUser = $user;
+ $this->mTimestamp = $timestamp;
+ $this->mMinorEdit = boolval( $row->ar_minor_edit );
+ $this->mDeleted = intval( $row->ar_deleted );
+ $this->mSize = isset( $row->ar_len ) ? intval( $row->ar_len ) : null;
+ $this->mSha1 = !empty( $row->ar_sha1 ) ? $row->ar_sha1 : null;
+ }
+
+ /**
+ * Get archive row ID
+ *
+ * @return int
+ */
+ public function getArchiveId() {
+ return $this->mArchiveId;
+ }
+
+ /**
+ * @return int|null The revision id, or null if the original revision ID
+ * was not recorded in the archive table.
+ */
+ public function getId() {
+ // overwritten just to refine the contract specification.
+ return parent::getId();
+ }
+
+ /**
+ * @throws RevisionAccessException if the size was unknown and could not be calculated.
+ * @return int The nominal revision size, never null. May be computed on the fly.
+ */
+ public function getSize() {
+ // If length is null, calculate and remember it (potentially SLOW!).
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+ * @return string The revision hash, never null. May be computed on the fly.
+ */
+ public function getSha1() {
+ // If hash is null, calculate it and remember (potentially SLOW!)
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return UserIdentity The identity of the revision author, null if access is forbidden.
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getUser( $audience, $user );
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return CommentStoreComment The revision comment, null if access is forbidden.
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getComment( $audience, $user );
+ }
+
+ /**
+ * @return string never null
+ */
+ public function getTimestamp() {
+ // overwritten just to add a guarantee to the contract
+ return parent::getTimestamp();
+ }
+
+ /**
+ * @see RevisionStore::isComplete
+ *
+ * @return bool always true.
+ */
+ public function isReadyForInsertion() {
+ return true;
+ }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionArchiveRecord::class, 'MediaWiki\Storage\RevisionArchiveRecord' );
--- /dev/null
+<?php
+/**
+ * Service for constructing revision objects.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use MWException;
+use Title;
+
+/**
+ * Service for constructing revision objects.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionFactory
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ */
+interface RevisionFactory {
+
+ /**
+ * Constructs a new RevisionRecord based on the given associative array following the MW1.29
+ * database convention for the Revision constructor.
+ *
+ * MCR migration note: this replaces Revision::newFromRow
+ *
+ * @deprecated since 1.31. Use a MutableRevisionRecord instead.
+ *
+ * @param array $fields
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title|null $title
+ *
+ * @return MutableRevisionRecord
+ * @throws MWException
+ */
+ public function newMutableRevisionFromArray( array $fields, $queryFlags = 0, Title $title = null );
+
+ /**
+ * Constructs a RevisionRecord given a database row and content slots.
+ *
+ * MCR migration note: this replaces Revision::newFromRow for rows based on the
+ * revision, slot, and content tables defined for MCR since MW1.31.
+ *
+ * @param object $row A query result row as a raw object.
+ * Use RevisionStore::getQueryInfo() to build a query that yields the required fields.
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title|null $title
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null );
+
+ /**
+ * 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 A query result row as a raw object.
+ * Use RevisionStore::getArchiveQueryInfo() to build a query that yields the
+ * required fields.
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title|null $title
+ * @param array $overrides An associative array that allows fields in $row to be overwritten.
+ * Keys in this array correspond to field names in $row without the "ar_" prefix, so
+ * $overrides['user'] will override $row->ar_user, etc.
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromArchiveRow(
+ $row,
+ $queryFlags = 0,
+ Title $title = null,
+ array $overrides = []
+ );
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionFactory::class, 'MediaWiki\Storage\RevisionFactory' );
--- /dev/null
+<?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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use IDBAccessObject;
+use MediaWiki\Linker\LinkTarget;
+use Title;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionLookup
+ */
+interface RevisionLookup extends IDBAccessObject {
+
+ /**
+ * 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:
+ *
+ * @param int $id
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionById( $id, $flags = 0 );
+
+ /**
+ * 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
+ *
+ * @param LinkTarget $linkTarget
+ * @param int $revId (optional)
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 );
+
+ /**
+ * 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
+ *
+ * @param int $pageId
+ * @param int $revId (optional)
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 );
+
+ /**
+ * Get previous revision for this title
+ *
+ * MCR migration note: this replaces Revision::getPrevious
+ *
+ * @param RevisionRecord $rev
+ * @param Title|null $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getPreviousRevision( RevisionRecord $rev, Title $title = null );
+
+ /**
+ * Get next revision for this title
+ *
+ * MCR migration note: this replaces Revision::getNext
+ *
+ * @param RevisionRecord $rev
+ * @param Title|null $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getNextRevision( RevisionRecord $rev, Title $title = null );
+
+ /**
+ * 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
+ *
+ * @return RevisionRecord|bool Returns false if missing
+ */
+ public function getKnownCurrentRevision( Title $title, $revId );
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionLookup::class, 'MediaWiki\Storage\RevisionLookup' );
--- /dev/null
+<?php
+/**
+ * Page revision base class.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Page revision base class.
+ *
+ * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
+ * Note that while the base class has no setters, subclasses may offer a mutable interface.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionRecord
+ */
+abstract class RevisionRecord {
+
+ // RevisionRecord deletion constants
+ const DELETED_TEXT = 1;
+ const DELETED_COMMENT = 2;
+ const DELETED_USER = 4;
+ const DELETED_RESTRICTED = 8;
+ const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED; // convenience
+ const SUPPRESSED_ALL = self::DELETED_TEXT | self::DELETED_COMMENT | self::DELETED_USER |
+ self::DELETED_RESTRICTED; // convenience
+
+ // Audience options for accessors
+ const FOR_PUBLIC = 1;
+ const FOR_THIS_USER = 2;
+ const RAW = 3;
+
+ /** @var string Wiki ID; false means the current wiki */
+ protected $mWiki = false;
+ /** @var int|null */
+ protected $mId;
+ /** @var int|null */
+ protected $mPageId;
+ /** @var UserIdentity|null */
+ protected $mUser;
+ /** @var bool */
+ protected $mMinorEdit = false;
+ /** @var string|null */
+ protected $mTimestamp;
+ /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
+ protected $mDeleted = 0;
+ /** @var int|null */
+ protected $mSize;
+ /** @var string|null */
+ protected $mSha1;
+ /** @var int|null */
+ protected $mParentId;
+ /** @var CommentStoreComment|null */
+ protected $mComment;
+
+ /** @var Title */
+ protected $mTitle; // TODO: we only need the title for permission checks!
+
+ /** @var RevisionSlots */
+ protected $mSlots;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ *
+ * @throws MWException
+ */
+ function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
+ Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+ $this->mTitle = $title;
+ $this->mSlots = $slots;
+ $this->mWiki = $wikiId;
+
+ // XXX: this is a sensible default, but we may not have a Title object here in the future.
+ $this->mPageId = $title->getArticleID();
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * @param RevisionRecord $rec
+ *
+ * @return bool True if this RevisionRecord is known to have same content as $rec.
+ * False if the content is different (or not known to be the same).
+ */
+ public function hasSameContent( RevisionRecord $rec ) {
+ if ( $rec === $this ) {
+ return true;
+ }
+
+ if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
+ return true;
+ }
+
+ // check size before hash, since size is quicker to compute
+ if ( $this->getSize() !== $rec->getSize() ) {
+ return false;
+ }
+
+ // instead of checking the hash, we could also check the content addresses of all slots.
+
+ if ( $this->getSha1() === $rec->getSha1() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the Content of the given slot of this revision.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * MCR migration note: this replaces Revision::getContent
+ *
+ * @param string $role The role name of the desired slot
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return Content|null The content of the given slot, or null if access is forbidden.
+ */
+ public function getContent( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+ // XXX: throwing an exception would be nicer, but would a further
+ // departure from the signature of Revision::getContent(), and thus
+ // more complex and error prone refactoring.
+ if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+ return null;
+ }
+
+ $content = $this->getSlot( $role, $audience, $user )->getContent();
+ return $content->copy();
+ }
+
+ /**
+ * Returns meta-data for the given slot.
+ *
+ * @param string $role The role name of the desired slot
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
+ * calling getContent() on the SlotRecord will throw an exception.
+ */
+ public function getSlot( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+ $slot = $this->mSlots->getSlot( $role );
+
+ if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+ return SlotRecord::newWithSuppressedContent( $slot );
+ }
+
+ return $slot;
+ }
+
+ /**
+ * Returns whether the given slot is defined in this revision.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @return bool
+ */
+ public function hasSlot( $role ) {
+ return $this->mSlots->hasSlot( $role );
+ }
+
+ /**
+ * Returns the slot names (roles) of all slots present in this revision.
+ * getContent() will succeed only for the names returned by this method.
+ *
+ * @return string[]
+ */
+ public function getSlotRoles() {
+ return $this->mSlots->getSlotRoles();
+ }
+
+ /**
+ * Returns the slots defined for this revision.
+ *
+ * @return RevisionSlots
+ */
+ public function getSlots() {
+ return $this->mSlots;
+ }
+
+ /**
+ * Returns the slots that originate in this revision.
+ *
+ * Note that this does not include any slots inherited from some earlier revision,
+ * even if they are different from the slots in the immediate parent revision.
+ * This is the case for rollbacks: slots of a rollback revision are inherited from
+ * the rollback target, and are different from the slots in the parent revision,
+ * which was rolled back.
+ *
+ * To find all slots modified by this revision against its immediate parent
+ * revision, use RevisionSlotsUpdate::newFromRevisionSlots().
+ *
+ * @return RevisionSlots
+ */
+ public function getOriginalSlots() {
+ return new RevisionSlots( $this->mSlots->getOriginalSlots() );
+ }
+
+ /**
+ * Returns slots inherited from some previous revision.
+ *
+ * "Inherited" slots are all slots that do not originate in this revision.
+ * Note that these slots may still differ from the one in the parent revision.
+ * This is the case for rollbacks: slots of a rollback revision are inherited from
+ * the rollback target, and are different from the slots in the parent revision,
+ * which was rolled back.
+ *
+ * @return RevisionSlots
+ */
+ public function getInheritedSlots() {
+ return new RevisionSlots( $this->mSlots->getInheritedSlots() );
+ }
+
+ /**
+ * Get revision ID. Depending on the concrete subclass, this may return null if
+ * the revision ID is not known (e.g. because the revision does not yet exist
+ * in the database).
+ *
+ * MCR migration note: this replaces Revision::getId
+ *
+ * @return int|null
+ */
+ public function getId() {
+ return $this->mId;
+ }
+
+ /**
+ * Get parent revision ID (the original previous page revision).
+ * If there is no parent revision, this returns 0.
+ * If the parent revision is undefined or unknown, this returns null.
+ *
+ * @note As of MW 1.31, the database schema allows the parent ID to be
+ * NULL to indicate that it is unknown.
+ *
+ * MCR migration note: this replaces Revision::getParentId
+ *
+ * @return int|null
+ */
+ public function getParentId() {
+ return $this->mParentId;
+ }
+
+ /**
+ * Returns the nominal size of this revision, in bogo-bytes.
+ * May be calculated on the fly if not known, which may in the worst
+ * case may involve loading all content.
+ *
+ * MCR migration note: this replaces Revision::getSize
+ *
+ * @throws RevisionAccessException if the size was unknown and could not be calculated.
+ * @return int
+ */
+ abstract public function getSize();
+
+ /**
+ * Returns the base36 sha1 of this revision. This hash is derived from the
+ * hashes of all slots associated with the revision.
+ * May be calculated on the fly if not known, which may in the worst
+ * case may involve loading all content.
+ *
+ * MCR migration note: this replaces Revision::getSha1
+ *
+ * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+ * @return string
+ */
+ abstract public function getSha1();
+
+ /**
+ * Get the page ID. If the page does not yet exist, the page ID is 0.
+ *
+ * MCR migration note: this replaces Revision::getPage
+ *
+ * @return int
+ */
+ public function getPageId() {
+ return $this->mPageId;
+ }
+
+ /**
+ * Get the ID of the wiki this revision belongs to.
+ *
+ * @return string|false The wiki's logical name, of false to indicate the local wiki.
+ */
+ public function getWikiId() {
+ return $this->mWiki;
+ }
+
+ /**
+ * Returns the title of the page this revision is associated with as a LinkTarget object.
+ *
+ * MCR migration note: this replaces Revision::getTitle
+ *
+ * @return LinkTarget
+ */
+ public function getPageAsLinkTarget() {
+ return $this->mTitle;
+ }
+
+ /**
+ * Fetch revision's author's user identity, if it's available to the specified audience.
+ * If the specified audience does not have access to it, null will be
+ * returned. Depending on the concrete subclass, null may also be returned if the user is
+ * not yet specified.
+ *
+ * MCR migration note: this replaces Revision::getUser
+ *
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the ID regardless of permissions
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @return UserIdentity|null
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
+ return null;
+ } else {
+ return $this->mUser;
+ }
+ }
+
+ /**
+ * Fetch revision comment, if it's available to the specified audience.
+ * If the specified audience does not have access to the comment,
+ * this will return null. Depending on the concrete subclass, null may also be returned
+ * if the comment is not yet specified.
+ *
+ * MCR migration note: this replaces Revision::getComment
+ *
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the text regardless of permissions
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ *
+ * @return CommentStoreComment|null
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
+ return null;
+ } else {
+ return $this->mComment;
+ }
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isMinor
+ *
+ * @return bool
+ */
+ public function isMinor() {
+ return (bool)$this->mMinorEdit;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isDeleted
+ *
+ * @param int $field One of DELETED_* bitfield constants
+ *
+ * @return bool
+ */
+ public function isDeleted( $field ) {
+ return ( $this->getVisibility() & $field ) == $field;
+ }
+
+ /**
+ * Get the deletion bitfield of the revision
+ *
+ * MCR migration note: this replaces Revision::getVisibility
+ *
+ * @return int
+ */
+ public function getVisibility() {
+ return (int)$this->mDeleted;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::getTimestamp.
+ *
+ * May return null if the timestamp was not specified.
+ *
+ * @return string|null
+ */
+ public function getTimestamp() {
+ return $this->mTimestamp;
+ }
+
+ /**
+ * Check that the given audience has access to the given field.
+ *
+ * MCR migration note: this corresponds to Revision::userCan
+ *
+ * @param int $field One of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the text regardless of permissions
+ * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
+ * ignored otherwise.
+ *
+ * @return bool
+ */
+ public function audienceCan( $field, $audience, User $user = null ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
+ return false;
+ } elseif ( $audience == self::FOR_THIS_USER ) {
+ if ( !$user ) {
+ throw new InvalidArgumentException(
+ 'A User object must be given when checking FOR_THIS_USER audience.'
+ );
+ }
+
+ if ( !$this->userCan( $field, $user ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted.
+ *
+ * MCR migration note: this corresponds to Revision::userCan
+ *
+ * @param int $field One of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @param User $user User object to check
+ * @return bool
+ */
+ protected function userCan( $field, User $user ) {
+ // TODO: use callback for permission checks, so we don't need to know a Title object!
+ return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted. This is used
+ * by various classes to avoid duplication.
+ *
+ * MCR migration note: this replaces Revision::userCanBitfield
+ *
+ * @param int $bitfield Current field
+ * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
+ * self::DELETED_COMMENT = File::DELETED_COMMENT,
+ * self::DELETED_USER = File::DELETED_USER
+ * @param User $user User object to check
+ * @param Title|null $title A Title object to check for per-page restrictions on,
+ * instead of just plain userrights
+ * @return bool
+ */
+ public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
+ if ( $bitfield & $field ) { // aspect is deleted
+ if ( $bitfield & self::DELETED_RESTRICTED ) {
+ $permissions = [ 'suppressrevision', 'viewsuppressed' ];
+ } elseif ( $field & self::DELETED_TEXT ) {
+ $permissions = [ 'deletedtext' ];
+ } else {
+ $permissions = [ 'deletedhistory' ];
+ }
+ $permissionlist = implode( ', ', $permissions );
+ if ( $title === null ) {
+ wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
+ return $user->isAllowedAny( ...$permissions );
+ } else {
+ $text = $title->getPrefixedText();
+ wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
+ foreach ( $permissions as $perm ) {
+ if ( $title->userCan( $perm, $user ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all
+ * information needed to save it to the database. This should trivially be true for
+ * RevisionRecords loaded from the database.
+ *
+ * Note that this may return true even if getId() or getPage() return null or 0, since these
+ * are generally assigned while the revision is saved to the database, and may not be available
+ * before.
+ *
+ * @return bool
+ */
+ public function isReadyForInsertion() {
+ // NOTE: don't check getSize() and getSha1(), since that may cause the full content to
+ // be loaded in order to calculate the values. Just assume these methods will not return
+ // null if mSlots is not empty.
+
+ // NOTE: getId() and getPageId() may return null before a revision is saved, so don't
+ //check them.
+
+ return $this->getTimestamp() !== null
+ && $this->getComment( self::RAW ) !== null
+ && $this->getUser( self::RAW ) !== null
+ && $this->mSlots->getSlotRoles() !== [];
+ }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionRecord::class, 'MediaWiki\Storage\RevisionRecord' );
use Html;
use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
use ParserOptions;
use ParserOutput;
use Psr\Log\LoggerInterface;
--- /dev/null
+<?php
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use Content;
+use LogicException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionSlots
+ */
+class RevisionSlots {
+
+ /** @var SlotRecord[]|callable */
+ protected $slots;
+
+ /**
+ * @param SlotRecord[]|callable $slots SlotRecords,
+ * or a callback that returns such a structure.
+ */
+ public function __construct( $slots ) {
+ Assert::parameterType( 'array|callable', $slots, '$slots' );
+
+ if ( is_callable( $slots ) ) {
+ $this->slots = $slots;
+ } else {
+ $this->setSlotsInternal( $slots );
+ }
+ }
+
+ /**
+ * @param SlotRecord[] $slots
+ */
+ private function setSlotsInternal( array $slots ) {
+ Assert::parameterElementType( SlotRecord::class, $slots, '$slots' );
+
+ $this->slots = [];
+
+ // re-key the slot array
+ foreach ( $slots as $slot ) {
+ $role = $slot->getRole();
+ $this->slots[$role] = $slot;
+ }
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * Returns the Content of the given slot.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return Content
+ */
+ public function getContent( $role ) {
+ // Return a copy to be safe. Immutable content objects return $this from copy().
+ return $this->getSlot( $role )->getContent()->copy();
+ }
+
+ /**
+ * Returns the SlotRecord of the given slot.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return SlotRecord
+ */
+ public function getSlot( $role ) {
+ $slots = $this->getSlots();
+
+ if ( isset( $slots[$role] ) ) {
+ return $slots[$role];
+ } else {
+ throw new RevisionAccessException( 'No such slot: ' . $role );
+ }
+ }
+
+ /**
+ * Returns whether the given slot is set.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @return bool
+ */
+ public function hasSlot( $role ) {
+ $slots = $this->getSlots();
+
+ return isset( $slots[$role] );
+ }
+
+ /**
+ * Returns the slot names (roles) of all slots present in this revision.
+ * getContent() will succeed only for the names returned by this method.
+ *
+ * @return string[]
+ */
+ public function getSlotRoles() {
+ $slots = $this->getSlots();
+ return array_keys( $slots );
+ }
+
+ /**
+ * Computes the total nominal size of the revision's slots, in bogo-bytes.
+ *
+ * @warning This is potentially expensive! It may cause all slot's content to be loaded
+ * and deserialized.
+ *
+ * @return int
+ */
+ public function computeSize() {
+ return array_reduce( $this->getSlots(), function ( $accu, SlotRecord $slot ) {
+ return $accu + $slot->getSize();
+ }, 0 );
+ }
+
+ /**
+ * Returns an associative array that maps role names to SlotRecords. Each SlotRecord
+ * represents the content meta-data of a slot, together they define the content of
+ * a revision.
+ *
+ * @note This may cause the content meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[] revision slot/content rows, keyed by slot role name.
+ */
+ public function getSlots() {
+ if ( is_callable( $this->slots ) ) {
+ $slots = call_user_func( $this->slots );
+
+ Assert::postcondition(
+ is_array( $slots ),
+ 'Slots info callback should return an array of objects'
+ );
+
+ $this->setSlotsInternal( $slots );
+ }
+
+ return $this->slots;
+ }
+
+ /**
+ * Computes the combined hash of the revisions's slots.
+ *
+ * @note For backwards compatibility, the combined hash of a single slot
+ * is that slot's hash. For consistency, the combined hash of an empty set of slots
+ * is the hash of the empty string.
+ *
+ * @warning This is potentially expensive! It may cause all slot's content to be loaded
+ * and deserialized, then re-serialized and hashed.
+ *
+ * @return string
+ */
+ public function computeSha1() {
+ $slots = $this->getSlots();
+ ksort( $slots );
+
+ if ( empty( $slots ) ) {
+ return SlotRecord::base36Sha1( '' );
+ }
+
+ return array_reduce( $slots, function ( $accu, SlotRecord $slot ) {
+ return $accu === null
+ ? $slot->getSha1()
+ : SlotRecord::base36Sha1( $accu . $slot->getSha1() );
+ }, null );
+ }
+
+ /**
+ * Return all slots that belong to the revision they originate from (that is,
+ * they are not inherited from some other revision).
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[]
+ */
+ public function getOriginalSlots() {
+ return array_filter(
+ $this->getSlots(),
+ function ( SlotRecord $slot ) {
+ return !$slot->isInherited();
+ }
+ );
+ }
+
+ /**
+ * Return all slots that are not not originate in the revision they belong to (that is,
+ * they are inherited from some other revision).
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[]
+ */
+ public function getInheritedSlots() {
+ return array_filter(
+ $this->getSlots(),
+ function ( SlotRecord $slot ) {
+ return $slot->isInherited();
+ }
+ );
+ }
+
+ /**
+ * Checks whether the other RevisionSlots instance has the same content
+ * as this instance. Note that this does not mean that the slots have to be the same:
+ * they could for instance belong to different revisions.
+ *
+ * @param RevisionSlots $other
+ *
+ * @return bool
+ */
+ public function hasSameContent( RevisionSlots $other ) {
+ if ( $other === $this ) {
+ return true;
+ }
+
+ $aSlots = $this->getSlots();
+ $bSlots = $other->getSlots();
+
+ ksort( $aSlots );
+ ksort( $bSlots );
+
+ if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) {
+ return false;
+ }
+
+ foreach ( $aSlots as $role => $s ) {
+ $t = $bSlots[$role];
+
+ if ( !$s->hasSameContent( $t ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Find roles for which the $other RevisionSlots object has different content
+ * as this RevisionSlots object, including any roles that are present in one
+ * but not the other.
+ *
+ * @param RevisionSlots $other
+ *
+ * @return string[] a list of slot roles that are different.
+ */
+ public function getRolesWithDifferentContent( RevisionSlots $other ) {
+ if ( $other === $this ) {
+ return [];
+ }
+
+ $aSlots = $this->getSlots();
+ $bSlots = $other->getSlots();
+
+ ksort( $aSlots );
+ ksort( $bSlots );
+
+ $different = array_keys( array_merge(
+ array_diff_key( $aSlots, $bSlots ),
+ array_diff_key( $bSlots, $aSlots )
+ ) );
+
+ /** @var SlotRecord[] $common */
+ $common = array_intersect_key( $aSlots, $bSlots );
+
+ foreach ( $common as $role => $s ) {
+ $t = $bSlots[$role];
+
+ if ( !$s->hasSameContent( $t ) ) {
+ $different[] = $role;
+ }
+ }
+
+ return $different;
+ }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionSlots::class, 'MediaWiki\Storage\RevisionSlots' );
--- /dev/null
+<?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
+ */
+
+namespace MediaWiki\Revision;
+
+use ActorMigration;
+use CommentStore;
+use CommentStoreComment;
+use Content;
+use ContentHandler;
+use DBAccessObjectUtils;
+use Hooks;
+use IDBAccessObject;
+use InvalidArgumentException;
+use IP;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Storage\BlobAccessException;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\NameTableAccessException;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use Message;
+use MWException;
+use MWUnknownContentModelException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use RecentChange;
+use Revision;
+use RuntimeException;
+use stdClass;
+use Title;
+use User;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ILoadBalancer;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionStore
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ */
+class RevisionStore
+ implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
+
+ const ROW_CACHE_KEY = 'revision-row-1.29';
+
+ /**
+ * @var SqlBlobStore
+ */
+ private $blobStore;
+
+ /**
+ * @var bool|string
+ */
+ private $wikiId;
+
+ /**
+ * @var boolean
+ * @see $wgContentHandlerUseDB
+ */
+ private $contentHandlerUseDB = true;
+
+ /**
+ * @var ILoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * @var WANObjectCache
+ */
+ private $cache;
+
+ /**
+ * @var CommentStore
+ */
+ private $commentStore;
+
+ /**
+ * @var ActorMigration
+ */
+ private $actorMigration;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @var NameTableStore
+ */
+ private $contentModelStore;
+
+ /**
+ * @var NameTableStore
+ */
+ private $slotRoleStore;
+
+ /** @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. */
+ private $mcrMigrationStage;
+
+ /**
+ * @todo $blobStore should be allowed to be any BlobStore!
+ *
+ * @param ILoadBalancer $loadBalancer
+ * @param SqlBlobStore $blobStore
+ * @param WANObjectCache $cache A cache for caching revision rows. This can be the local
+ * wiki's default instance even if $wikiId refers to a different wiki, since
+ * 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.
+ * @param CommentStore $commentStore
+ * @param NameTableStore $contentModelStore
+ * @param NameTableStore $slotRoleStore
+ * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags
+ * @param ActorMigration $actorMigration
+ * @param bool|string $wikiId
+ *
+ * @throws MWException if $mcrMigrationStage or $wikiId is invalid.
+ */
+ public function __construct(
+ ILoadBalancer $loadBalancer,
+ SqlBlobStore $blobStore,
+ WANObjectCache $cache,
+ CommentStore $commentStore,
+ NameTableStore $contentModelStore,
+ NameTableStore $slotRoleStore,
+ $mcrMigrationStage,
+ ActorMigration $actorMigration,
+ $wikiId = false
+ ) {
+ Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+ Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
+ '$mcrMigrationStage',
+ 'Reading from the old and the new schema at the same time is not supported.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== 0,
+ '$mcrMigrationStage',
+ 'Reading needs to be enabled for the old or the new schema.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) !== 0,
+ '$mcrMigrationStage',
+ 'Writing needs to be enabled for the old or the new schema.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_OLD ) === 0
+ || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) !== 0,
+ '$mcrMigrationStage',
+ 'Cannot read the old schema when not also writing it.'
+ );
+ Assert::parameter(
+ ( $mcrMigrationStage & SCHEMA_COMPAT_READ_NEW ) === 0
+ || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) !== 0,
+ '$mcrMigrationStage',
+ 'Cannot read the new schema when not also writing it.'
+ );
+
+ $this->loadBalancer = $loadBalancer;
+ $this->blobStore = $blobStore;
+ $this->cache = $cache;
+ $this->commentStore = $commentStore;
+ $this->contentModelStore = $contentModelStore;
+ $this->slotRoleStore = $slotRoleStore;
+ $this->mcrMigrationStage = $mcrMigrationStage;
+ $this->actorMigration = $actorMigration;
+ $this->wikiId = $wikiId;
+ $this->logger = new NullLogger();
+ }
+
+ /**
+ * @param int $flags A combination of SCHEMA_COMPAT_XXX flags.
+ * @return bool True if all the given flags were set in the $mcrMigrationStage
+ * parameter passed to the constructor.
+ */
+ private function hasMcrSchemaFlags( $flags ) {
+ return ( $this->mcrMigrationStage & $flags ) === $flags;
+ }
+
+ /**
+ * Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading
+ * and still reading from the old DB schema.
+ *
+ * @throws RevisionAccessException
+ */
+ private function assertCrossWikiContentLoadingIsSafe() {
+ if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ throw new RevisionAccessException(
+ "Cross-wiki content loading is not supported by the pre-MCR schema"
+ );
+ }
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return bool Whether the store is read-only
+ */
+ public function isReadOnly() {
+ return $this->blobStore->isReadOnly();
+ }
+
+ /**
+ * @return bool
+ */
+ public function getContentHandlerUseDB() {
+ return $this->contentHandlerUseDB;
+ }
+
+ /**
+ * @see $wgContentHandlerUseDB
+ * @param bool $contentHandlerUseDB
+ * @throws MWException
+ */
+ public function setContentHandlerUseDB( $contentHandlerUseDB ) {
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
+ || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
+ ) {
+ if ( !$contentHandlerUseDB ) {
+ throw new MWException(
+ 'Content model must be stored in the database for multi content revision migration.'
+ );
+ }
+ }
+ $this->contentHandlerUseDB = $contentHandlerUseDB;
+ }
+
+ /**
+ * @return ILoadBalancer
+ */
+ private function getDBLoadBalancer() {
+ return $this->loadBalancer;
+ }
+
+ /**
+ * @param int $mode DB_MASTER or DB_REPLICA
+ *
+ * @return IDatabase
+ */
+ private function getDBConnection( $mode ) {
+ $lb = $this->getDBLoadBalancer();
+ return $lb->getConnection( $mode, [], $this->wikiId );
+ }
+
+ /**
+ * @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 );
+ }
+
+ /**
+ * @param IDatabase $connection
+ */
+ private function releaseDBConnection( IDatabase $connection ) {
+ $lb = $this->getDBLoadBalancer();
+ $lb->reuseConnection( $connection );
+ }
+
+ /**
+ * @param int $mode DB_MASTER or DB_REPLICA
+ *
+ * @return DBConnRef
+ */
+ private function getDBConnectionRef( $mode ) {
+ $lb = $this->getDBLoadBalancer();
+ return $lb->getConnectionRef( $mode, [], $this->wikiId );
+ }
+
+ /**
+ * Determines the page Title based on the available information.
+ *
+ * MCR migration note: this corresponds to Revision::getTitle
+ *
+ * @note this method should be private, external use should be avoided!
+ *
+ * @param int|null $pageId
+ * @param int|null $revId
+ * @param int $queryFlags
+ *
+ * @return Title
+ * @throws RevisionAccessException
+ */
+ public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
+ if ( !$pageId && !$revId ) {
+ throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
+ }
+
+ // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
+ // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
+ if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
+ $queryFlags = self::READ_NORMAL;
+ }
+
+ $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
+ list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+ $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
+
+ // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
+ if ( $canUseTitleNewFromId ) {
+ // TODO: better foreign title handling (introduce TitleFactory)
+ $title = Title::newFromID( $pageId, $titleFlags );
+ if ( $title ) {
+ return $title;
+ }
+ }
+
+ // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
+ $canUseRevId = ( $revId !== null && $revId > 0 );
+
+ if ( $canUseRevId ) {
+ $dbr = $this->getDBConnectionRef( $dbMode );
+ // @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)
+ return Title::newFromRow( $row );
+ }
+ }
+
+ // If we still don't have a title, fallback to master if that wasn't already happening.
+ if ( $dbMode !== DB_MASTER ) {
+ $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
+ if ( $title ) {
+ $this->logger->info(
+ __METHOD__ . ' fell back to READ_LATEST and got a Title.',
+ [ 'trace' => wfBacktrace() ]
+ );
+ return $title;
+ }
+ }
+
+ throw new RevisionAccessException(
+ "Could not determine title for page ID $pageId and revision ID $revId"
+ );
+ }
+
+ /**
+ * @param mixed $value
+ * @param string $name
+ *
+ * @throws IncompleteRevisionException if $value is null
+ * @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
+ *
+ * @throws IncompleteRevisionException if $value is empty
+ * @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;
+ }
+
+ /**
+ * Insert a new revision into the database, returning the new revision record
+ * on success and dies horribly on failure.
+ *
+ * MCR migration note: this replaces Revision::insertOn
+ *
+ * @param RevisionRecord $rev
+ * @param IDatabase $dbw (master connection)
+ *
+ * @throws InvalidArgumentException
+ * @return RevisionRecord the new revision record.
+ */
+ public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
+ // TODO: pass in a DBTransactionContext instead of a database connection.
+ $this->checkDatabaseWikiId( $dbw );
+
+ $slotRoles = $rev->getSlotRoles();
+
+ // Make sure the main slot is always provided throughout migration
+ if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
+ throw new InvalidArgumentException(
+ 'main slot must be provided'
+ );
+ }
+
+ // If we are not writing into the new schema, we can't support extra slots.
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
+ && $slotRoles !== [ SlotRecord::MAIN ]
+ ) {
+ throw new InvalidArgumentException(
+ 'Only the main slot is supported when not writing to the MCR enabled schema!'
+ );
+ }
+
+ // As long as we are not reading from the new schema, we don't want to write extra slots.
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
+ && $slotRoles !== [ SlotRecord::MAIN ]
+ ) {
+ throw new InvalidArgumentException(
+ 'Only the main slot is supported when not reading from the MCR enabled schema!'
+ );
+ }
+
+ // 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' );
+
+ 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' );
+ }
+
+ // 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();
+
+ /** @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
+ );
+ }
+ );
+
+ // 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'
+ );
+
+ // Trigger exception if the main slot is missing.
+ // Technically, this could go away after MCR migration: while
+ // calling code may require a main slot to exist, RevisionStore
+ // really should not know or care about that requirement.
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
+
+ foreach ( $slotRoles as $role ) {
+ $slot = $rev->getSlot( $role, RevisionRecord::RAW );
+ Assert::postcondition(
+ $slot->getContent() !== null,
+ $role . ' slot must have content'
+ );
+ Assert::postcondition(
+ $slot->hasRevision(),
+ $role . ' slot must have a revision associated'
+ );
+ }
+
+ Hooks::run( 'RevisionRecordInserted', [ $rev ] );
+
+ // TODO: deprecate in 1.32!
+ $legacyRevision = new Revision( $rev );
+ Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
+
+ return $rev;
+ }
+
+ private function insertRevisionInternal(
+ RevisionRecord $rev,
+ IDatabase $dbw,
+ User $user,
+ CommentStoreComment $comment,
+ Title $title,
+ $pageId,
+ $parentId
+ ) {
+ $slotRoles = $rev->getSlotRoles();
+
+ $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 );
+
+ // 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() ) {
+ // 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;
+
+ // Write the main slot's text ID to the revision table for backwards compatibility
+ if ( $slot->getRole() === SlotRecord::MAIN
+ && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
+ ) {
+ $blobAddress = $slot->getAddress();
+ $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+ }
+ } else {
+ $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
+ }
+ }
+
+ $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
+
+ $rev = new RevisionStoreRecord(
+ $title,
+ $user,
+ $comment,
+ (object)$revisionRow,
+ new RevisionSlots( $newSlots ),
+ $this->wikiId
+ );
+
+ return $rev;
+ }
+
+ /**
+ * @param IDatabase $dbw
+ * @param int $revisionId
+ * @param string &$blobAddress (may change!)
+ *
+ * @return int the text row id
+ */
+ private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
+ $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
+ if ( !$textId ) {
+ throw new LogicException(
+ 'Blob address not supported in 1.29 database schema: ' . $blobAddress
+ );
+ }
+
+ // getTextIdFromAddress() is free to insert something into the text table, so $textId
+ // may be a new value, not anything already contained in $blobAddress.
+ $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
+
+ $dbw->update(
+ 'revision',
+ [ 'rev_text_id' => $textId ],
+ [ 'rev_id' => $revisionId ],
+ __METHOD__
+ );
+
+ return $textId;
+ }
+
+ /**
+ * @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();
+ } else {
+ $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
+ }
+
+ $contentId = null;
+
+ // Write the main slot's text ID to the revision table for backwards compatibility
+ if ( $protoSlot->getRole() === SlotRecord::MAIN
+ && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
+ ) {
+ // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
+ // with the real content ID below.
+ $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+ $contentId = $this->emulateContentId( $textId );
+ }
+
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+ if ( $protoSlot->hasContentId() ) {
+ $contentId = $protoSlot->getContentId();
+ } else {
+ $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
+ }
+
+ $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
+ }
+
+ $savedSlot = SlotRecord::newSaved(
+ $revisionId,
+ $contentId,
+ $blobAddress,
+ $protoSlot
+ );
+
+ return $savedSlot;
+ }
+
+ /**
+ * 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
+ ) {
+ if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
+ $ipcRow = [
+ 'ipc_rev_id' => $revisionId,
+ 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
+ 'ipc_hex' => IP::toHex( $user->getName() ),
+ ];
+ $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
+ }
+ }
+
+ /**
+ * @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 );
+
+ list( $commentFields, $commentCallback ) =
+ $this->commentStore->insertWithTempTable(
+ $dbw,
+ 'rev_comment',
+ $rev->getComment( RevisionRecord::RAW )
+ );
+ $revisionRow += $commentFields;
+
+ list( $actorFields, $actorCallback ) =
+ $this->actorMigration->getInsertValuesWithTempTable(
+ $dbw,
+ 'rev_user',
+ $rev->getUser( RevisionRecord::RAW )
+ );
+ $revisionRow += $actorFields;
+
+ $dbw->insert( 'revision', $revisionRow, __METHOD__ );
+
+ if ( !isset( $revisionRow['rev_id'] ) ) {
+ // only if auto-increment was used
+ $revisionRow['rev_id'] = intval( $dbw->insertId() );
+
+ 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';
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+ $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
+ if ( $maxRevId2 >= $maxRevId ) {
+ $maxRevId = $maxRevId2;
+ $table = 'slots';
+ }
+ }
+
+ 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__;
+ $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
+ $dbw->unlock( 'fix-for-T202032', $fname );
+ } );
+
+ $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(
+ $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
+ )->fetchObject();
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+ $row2 = $dbw->query(
+ $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
+ . ' FOR UPDATE'
+ )->fetchObject();
+ } else {
+ $row2 = null;
+ }
+ $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__ );
+ }
+ }
+ }
+
+ $commentCallback( $revisionRow['rev_id'] );
+ $actorCallback( $revisionRow['rev_id'], $revisionRow );
+
+ return $revisionRow;
+ }
+
+ /**
+ * @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(),
+ ];
+
+ if ( $rev->getId() !== null ) {
+ // Needed to restore revisions with their original ID
+ $revisionRow['rev_id'] = $rev->getId();
+ }
+
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
+ // In non MCR mode this IF section will relate to the main slot
+ $mainSlot = $rev->getSlot( SlotRecord::MAIN );
+ $model = $mainSlot->getModel();
+ $format = $mainSlot->getFormat();
+
+ // MCR migration note: rev_content_model and rev_content_format will go away
+ if ( $this->contentHandlerUseDB ) {
+ $this->assertCrossWikiContentLoadingIsSafe();
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+ $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+ $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+ }
+ }
+
+ return $revisionRow;
+ }
+
+ /**
+ * @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();
+
+ $this->checkContent( $content, $title );
+
+ 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,
+ ]
+ )
+ );
+ }
+
+ /**
+ * @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() );
+ }
+
+ /**
+ * MCR migration note: this corresponds to Revision::checkContentModel
+ *
+ * @param Content $content
+ * @param Title $title
+ *
+ * @throws MWException
+ * @throws MWUnknownContentModelException
+ */
+ private function checkContent( Content $content, Title $title ) {
+ // 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 ( !$this->contentHandlerUseDB ) {
+ // if $wgContentHandlerUseDB is not set,
+ // all revisions must use the default content model and format.
+
+ $this->assertCrossWikiContentLoadingIsSafe();
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultHandler = ContentHandler::getForModelID( $defaultModel );
+ $defaultFormat = $defaultHandler->getDefaultFormat();
+
+ if ( $model != $defaultModel ) {
+ throw new MWException( "Can't save non-default content model with "
+ . "\$wgContentHandlerUseDB disabled: model is $model, "
+ . "default for $name is $defaultModel"
+ );
+ }
+
+ if ( $format != $defaultFormat ) {
+ throw new MWException( "Can't use non-default content format with "
+ . "\$wgContentHandlerUseDB disabled: format is $format, "
+ . "default for $name is $defaultFormat"
+ );
+ }
+ }
+
+ 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.
+ *
+ * @note This method grabs a FOR UPDATE lock on the relevant row of the page table,
+ * to prevent a new revision from being inserted before the null revision has been written
+ * to the database.
+ *
+ * MCR migration note: this replaces Revision::newNullRevision
+ *
+ * @todo Introduce newFromParentRevision(). newNullRevision can then be based on that
+ * (or go away).
+ *
+ * @param IDatabase $dbw used for obtaining the lock on the page table row
+ * @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
+ *
+ * @return RevisionRecord|null RevisionRecord or null on error
+ */
+ public function newNullRevision(
+ IDatabase $dbw,
+ Title $title,
+ CommentStoreComment $comment,
+ $minor,
+ User $user
+ ) {
+ $this->checkDatabaseWikiId( $dbw );
+
+ $pageId = $title->getArticleID();
+
+ // 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',
+ [ 'page_id' => $pageId ],
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+
+ if ( !$pageLatest ) {
+ return null;
+ }
+
+ // Fetch the actual revision row from master, without locking all extra tables.
+ $oldRevision = $this->loadRevisionFromConds(
+ $dbw,
+ [ 'rev_id' => intval( $pageLatest ) ],
+ self::READ_LATEST,
+ $title
+ );
+
+ if ( !$oldRevision ) {
+ $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
+ $this->logger->error(
+ $msg,
+ [ 'exception' => new RuntimeException( $msg ) ]
+ );
+ return null;
+ }
+
+ // Construct the new revision
+ $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
+ $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
+
+ $newRevision->setComment( $comment );
+ $newRevision->setUser( $user );
+ $newRevision->setTimestamp( $timestamp );
+ $newRevision->setMinorEdit( $minor );
+
+ return $newRevision;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isUnpatrolled
+ *
+ * @todo This is overly specific, so move or kill this method.
+ *
+ * @param RevisionRecord $rev
+ *
+ * @return int Rcid of the unpatrolled row, zero if there isn't one
+ */
+ public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
+ $rc = $this->getRecentChange( $rev );
+ if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
+ 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 );
+ $db = $this->getDBConnection( $dbType );
+
+ $userIdentity = $rev->getUser( RevisionRecord::RAW );
+
+ if ( !$userIdentity ) {
+ // If the revision has no user identity, chances are it never went
+ // into the database, and doesn't have an RC entry.
+ return null;
+ }
+
+ // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
+ $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
+ $rc = RecentChange::newFromConds(
+ [
+ $actorWhere['conds'],
+ 'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
+ 'rc_this_oldid' => $rev->getId()
+ ],
+ __METHOD__,
+ $dbType
+ );
+
+ $this->releaseDBConnection( $db );
+
+ // XXX: cache this locally? Glue it to the RevisionRecord?
+ return $rc;
+ }
+
+ /**
+ * Maps fields of the archive row to corresponding revision rows.
+ *
+ * @param object $archiveRow
+ *
+ * @return object a revision row object, corresponding to $archiveRow.
+ */
+ private static function mapArchiveFields( $archiveRow ) {
+ $fieldMap = [
+ // keep with ar prefix:
+ 'ar_id' => 'ar_id',
+
+ // not the same suffix:
+ 'ar_page_id' => 'rev_page',
+ 'ar_rev_id' => 'rev_id',
+
+ // same suffix:
+ 'ar_text_id' => 'rev_text_id',
+ 'ar_timestamp' => 'rev_timestamp',
+ 'ar_user_text' => 'rev_user_text',
+ 'ar_user' => 'rev_user',
+ 'ar_actor' => 'rev_actor',
+ 'ar_minor_edit' => 'rev_minor_edit',
+ 'ar_deleted' => 'rev_deleted',
+ 'ar_len' => 'rev_len',
+ 'ar_parent_id' => 'rev_parent_id',
+ 'ar_sha1' => 'rev_sha1',
+ 'ar_comment' => 'rev_comment',
+ 'ar_comment_cid' => 'rev_comment_cid',
+ 'ar_comment_id' => 'rev_comment_id',
+ 'ar_comment_text' => 'rev_comment_text',
+ 'ar_comment_data' => 'rev_comment_data',
+ 'ar_comment_old' => 'rev_comment_old',
+ 'ar_content_format' => 'rev_content_format',
+ 'ar_content_model' => 'rev_content_model',
+ ];
+
+ $revRow = new stdClass();
+ foreach ( $fieldMap as $arKey => $revKey ) {
+ if ( property_exists( $archiveRow, $arKey ) ) {
+ $revRow->$revKey = $archiveRow->$arKey;
+ }
+ }
+
+ return $revRow;
+ }
+
+ /**
+ * Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
+ *
+ * @param object|array $row Either a database row or an array
+ * @param int $queryFlags for callbacks
+ * @param Title $title
+ *
+ * @return SlotRecord The main slot, extracted from the MW 1.29 style row.
+ * @throws MWException
+ */
+ private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
+ $mainSlotRow = new stdClass();
+ $mainSlotRow->role_name = SlotRecord::MAIN;
+ $mainSlotRow->model_name = null;
+ $mainSlotRow->slot_revision_id = null;
+ $mainSlotRow->slot_content_id = null;
+ $mainSlotRow->content_address = null;
+
+ $content = null;
+ $blobData = null;
+ $blobFlags = null;
+
+ if ( is_object( $row ) ) {
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
+ // Don't emulate from a row when using the new schema.
+ // Emulating from an array is still OK.
+ throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
+ }
+
+ // archive row
+ if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
+ $row = $this->mapArchiveFields( $row );
+ }
+
+ if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
+ $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
+ $row->rev_text_id
+ );
+ }
+
+ // This is used by null-revisions
+ $mainSlotRow->slot_origin = isset( $row->slot_origin )
+ ? intval( $row->slot_origin )
+ : null;
+
+ if ( isset( $row->old_text ) ) {
+ // this happens when the text-table gets joined directly, in the pre-1.30 schema
+ $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
+ // Check against selects that might have not included old_flags
+ if ( !property_exists( $row, 'old_flags' ) ) {
+ throw new InvalidArgumentException( 'old_flags was not set in $row' );
+ }
+ $blobFlags = $row->old_flags ?? '';
+ }
+
+ $mainSlotRow->slot_revision_id = intval( $row->rev_id );
+
+ $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
+ $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
+ $mainSlotRow->model_name = isset( $row->rev_content_model )
+ ? strval( $row->rev_content_model )
+ : null;
+ // XXX: in the future, we'll probably always use the default format, and drop content_format
+ $mainSlotRow->format_name = isset( $row->rev_content_format )
+ ? strval( $row->rev_content_format )
+ : null;
+
+ if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
+ // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+ $mainSlotRow->slot_content_id
+ = $this->emulateContentId( intval( $row->rev_text_id ) );
+ }
+ } elseif ( is_array( $row ) ) {
+ $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
+
+ $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
+ ? intval( $row['slot_origin'] )
+ : null;
+ $mainSlotRow->content_address = isset( $row['text_id'] )
+ ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
+ : null;
+ $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
+ $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
+
+ $mainSlotRow->model_name = isset( $row['content_model'] )
+ ? strval( $row['content_model'] ) : null; // XXX: must be a string!
+ // XXX: in the future, we'll probably always use the default format, and drop content_format
+ $mainSlotRow->format_name = isset( $row['content_format'] )
+ ? strval( $row['content_format'] ) : null;
+ $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
+ // XXX: If the flags field is not set then $blobFlags should be null so that no
+ // decoding will happen. An empty string will result in default decodings.
+ $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
+
+ // if we have a Content object, override mText and mContentModel
+ if ( !empty( $row['content'] ) ) {
+ if ( !( $row['content'] instanceof Content ) ) {
+ throw new MWException( 'content field must contain a Content object.' );
+ }
+
+ /** @var Content $content */
+ $content = $row['content'];
+ $handler = $content->getContentHandler();
+
+ $mainSlotRow->model_name = $content->getModel();
+
+ // XXX: in the future, we'll probably always use the default format.
+ if ( $mainSlotRow->format_name === null ) {
+ $mainSlotRow->format_name = $handler->getDefaultFormat();
+ }
+ }
+
+ if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
+ // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+ $mainSlotRow->slot_content_id
+ = $this->emulateContentId( intval( $row['text_id'] ) );
+ }
+ } else {
+ throw new MWException( 'Revision constructor passed invalid row format.' );
+ }
+
+ // With the old schema, the content changes with every revision,
+ // except for null-revisions.
+ if ( !isset( $mainSlotRow->slot_origin ) ) {
+ $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
+ }
+
+ if ( $mainSlotRow->model_name === null ) {
+ $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
+ $this->assertCrossWikiContentLoadingIsSafe();
+
+ // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
+ // TODO: MCR: deprecate $title->getModel().
+ return ContentHandler::getDefaultModelFor( $title );
+ };
+ }
+
+ if ( !$content ) {
+ // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
+ // is missing, but "empty revisions" with no content are used in some edge cases.
+
+ $content = function ( SlotRecord $slot )
+ use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
+ {
+ return $this->loadSlotContent(
+ $slot,
+ $blobData,
+ $blobFlags,
+ $mainSlotRow->format_name,
+ $queryFlags
+ );
+ };
+ }
+
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+ // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
+ // the inherited slot to have the same content_id as the original slot. In that case,
+ // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
+ $mainSlotRow->slot_content_id =
+ function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
+ };
+ }
+
+ return new SlotRecord( $mainSlotRow, $content );
+ }
+
+ /**
+ * Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode,
+ * based on the revision's text ID (rev_text_id or ar_text_id, respectively).
+ * Note that in SCHEMA_COMPAT_WRITE_BOTH, a callback to findSlotContentId() should be used
+ * instead, since in that mode, some revision rows may already have a real content ID,
+ * while other's don't - and for the ones that don't, we should indicate that it
+ * is missing and cause SlotRecords::hasContentId() to return false.
+ *
+ * @param int $textId
+ * @return int The emulated content ID
+ */
+ private function emulateContentId( $textId ) {
+ // Return a negative number to ensure the ID is distinct from any real content IDs
+ // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
+ // mode.
+ return -$textId;
+ }
+
+ /**
+ * 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
+ * @param string|null $blobFlags Flags indicating how $blobData needs to be processed.
+ * Use null if no processing should happen. That is in constrast to the empty string,
+ * which causes the blob to be decoded according to the configured legacy encoding.
+ * @param string|null $blobFormat MIME type indicating how $dataBlob is encoded
+ * @param int $queryFlags
+ *
+ * @throws RevisionAccessException
+ * @return Content
+ */
+ private function loadSlotContent(
+ SlotRecord $slot,
+ $blobData = null,
+ $blobFlags = null,
+ $blobFormat = null,
+ $queryFlags = 0
+ ) {
+ if ( $blobData !== null ) {
+ Assert::parameterType( 'string', $blobData, '$blobData' );
+ Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
+
+ $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
+
+ if ( $blobFlags === null ) {
+ // No blob flags, so use the blob verbatim.
+ $data = $blobData;
+ } else {
+ $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
+ if ( $data === false ) {
+ throw new RevisionAccessException(
+ "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
+ );
+ }
+ }
+
+ } else {
+ $address = $slot->getAddress();
+ try {
+ $data = $this->blobStore->getBlob( $address, $queryFlags );
+ } catch ( BlobAccessException $e ) {
+ throw new RevisionAccessException(
+ "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
+ );
+ }
+ }
+
+ // Unserialize content
+ $handler = ContentHandler::getForModelID( $slot->getModel() );
+
+ $content = $handler->unserializeContent( $data, $blobFormat );
+ return $content;
+ }
+
+ /**
+ * 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
+ */
+ public function getRevisionById( $id, $flags = 0 ) {
+ return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
+ }
+
+ /**
+ * 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 ) {
+ $conds = [
+ 'page_namespace' => $linkTarget->getNamespace(),
+ 'page_title' => $linkTarget->getDBkey()
+ ];
+ 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.
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
+
+ $conds[] = 'rev_id=page_latest';
+ $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
+
+ 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.
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
+
+ $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
+ *
+ * @param Title $title
+ * @param string $timestamp
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByTimestamp( $title, $timestamp ) {
+ $db = $this->getDBConnection( DB_REPLICA );
+ return $this->newRevisionFromConds(
+ [
+ 'rev_timestamp' => $db->timestamp( $timestamp ),
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ],
+ 0,
+ $title
+ );
+ }
+
+ /**
+ * @param int $revId The revision to load slots for.
+ * @param int $queryFlags
+ *
+ * @return SlotRecord[]
+ */
+ private function loadSlotRecords( $revId, $queryFlags ) {
+ $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']
+ );
+
+ $slots = [];
+
+ foreach ( $res as $row ) {
+ // resolve role names and model names from in-memory cache, instead of joining.
+ $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
+ $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
+
+ $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
+ return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
+ };
+
+ $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
+ }
+
+ if ( !isset( $slots[SlotRecord::MAIN] ) ) {
+ throw new RevisionAccessException(
+ 'Main slot of revision ' . $revId . ' not found in database!'
+ );
+ };
+
+ return $slots;
+ }
+
+ /**
+ * Factory method for RevisionSlots.
+ *
+ * @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
+ * @param int $queryFlags
+ * @param Title $title
+ *
+ * @return RevisionSlots
+ * @throws MWException
+ */
+ private function newRevisionSlots(
+ $revId,
+ $revisionRow,
+ $queryFlags,
+ Title $title
+ ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
+ $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
+ $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
+ } else {
+ // XXX: do we need the same kind of caching here
+ // that getKnownCurrentRevision uses (if $revId == page_latest?)
+
+ $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
+ return $this->loadSlotRecords( $revId, $queryFlags );
+ } );
+ }
+
+ return $slots;
+ }
+
+ /**
+ * 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 = []
+ ) {
+ 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;
+ }
+
+ try {
+ $user = User::newFromAnyId(
+ $row->ar_user ?? null,
+ $row->ar_user_text ?? null,
+ $row->ar_actor ?? null
+ );
+ } catch ( InvalidArgumentException $ex ) {
+ wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+ $user = new UserIdentityValue( 0, 'Unknown user', 0 );
+ }
+
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ // Legacy because $row may have come from self::selectFields()
+ $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
+
+ $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
+
+ return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
+ }
+
+ /**
+ * @see RevisionFactory::newRevisionFromRow
+ *
+ * MCR migration note: this replaces Revision::newFromRow
+ *
+ * @param object $row
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
+ Assert::parameterType( 'object', $row, '$row' );
+
+ if ( !$title ) {
+ $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
+ $revId = $row->rev_id ?? 0;
+
+ $title = $this->getTitle( $pageId, $revId, $queryFlags );
+ }
+
+ 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() );
+ }
+ }
+
+ try {
+ $user = User::newFromAnyId(
+ $row->rev_user ?? null,
+ $row->rev_user_text ?? null,
+ $row->rev_actor ?? null
+ );
+ } catch ( InvalidArgumentException $ex ) {
+ wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+ $user = new UserIdentityValue( 0, 'Unknown user', 0 );
+ }
+
+ $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+ // Legacy because $row may have come from self::selectFields()
+ $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
+
+ $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
+
+ return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
+ }
+
+ /**
+ * 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 ) {
+ $pageId = $fields['page'] ?? 0;
+ $revId = $fields['id'] ?? 0;
+
+ $title = $this->getTitle( $pageId, $revId, $queryFlags );
+ }
+
+ if ( !isset( $fields['page'] ) ) {
+ $fields['page'] = $title->getArticleID( $queryFlags );
+ }
+
+ // if we have a content object, use it to set the model and type
+ if ( !empty( $fields['content'] ) ) {
+ if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
+ throw new MWException(
+ 'content field must contain a Content object or an array of Content objects.'
+ );
+ }
+ }
+
+ if ( !empty( $fields['text_id'] ) ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ throw new MWException( "The text_id field is only available in the pre-MCR schema" );
+ }
+
+ if ( !empty( $fields['content'] ) ) {
+ throw new MWException(
+ "Text already stored in external store (id {$fields['text_id']}), " .
+ "can't specify content object"
+ );
+ }
+ }
+
+ if (
+ isset( $fields['comment'] )
+ && !( $fields['comment'] instanceof CommentStoreComment )
+ ) {
+ $commentData = $fields['comment_data'] ?? null;
+
+ if ( $fields['comment'] instanceof Message ) {
+ $fields['comment'] = CommentStoreComment::newUnsavedComment(
+ $fields['comment'],
+ $commentData
+ );
+ } else {
+ $commentText = trim( strval( $fields['comment'] ) );
+ $fields['comment'] = CommentStoreComment::newUnsavedComment(
+ $commentText,
+ $commentData
+ );
+ }
+ }
+
+ $revision = new MutableRevisionRecord( $title, $this->wikiId );
+ $this->initializeMutableRevisionFromArray( $revision, $fields );
+
+ if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
+ foreach ( $fields['content'] as $role => $content ) {
+ $revision->setContent( $role, $content );
+ }
+ } else {
+ $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
+ $revision->setSlot( $mainSlot );
+ }
+
+ return $revision;
+ }
+
+ /**
+ * @param MutableRevisionRecord $record
+ * @param array $fields
+ */
+ private function initializeMutableRevisionFromArray(
+ MutableRevisionRecord $record,
+ array $fields
+ ) {
+ /** @var UserIdentity $user */
+ $user = null;
+
+ if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
+ $user = $fields['user'];
+ } else {
+ try {
+ $user = User::newFromAnyId(
+ $fields['user'] ?? null,
+ $fields['user_text'] ?? null,
+ $fields['actor'] ?? null
+ );
+ } catch ( InvalidArgumentException $ex ) {
+ $user = null;
+ }
+ }
+
+ if ( $user ) {
+ $record->setUser( $user );
+ }
+
+ $timestamp = isset( $fields['timestamp'] )
+ ? strval( $fields['timestamp'] )
+ : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
+
+ $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'] );
+ }
+ if ( isset( $fields['size'] ) ) {
+ $record->setSize( intval( $fields['size'] ) );
+ }
+
+ 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 a page revision from a given revision ID number.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this corresponds to Revision::loadFromId
+ *
+ * @note direct use is deprecated!
+ * @todo remove when unused! there seem to be no callers of Revision::loadFromId
+ *
+ * @param IDatabase $db
+ * @param int $id
+ *
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromId( IDatabase $db, $id ) {
+ return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
+ }
+
+ /**
+ * 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
+ *
+ * @note direct use is deprecated!
+ * @todo remove when unused!
+ *
+ * @param IDatabase $db
+ * @param int $pageid
+ * @param int $id
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
+ $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!
+ *
+ * @param IDatabase $db
+ * @param Title $title
+ * @param int $id
+ *
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
+ 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
+ *
+ * @note direct use is deprecated! Use getRevisionFromTimestamp instead!
+ * @todo remove when unused!
+ *
+ * @param IDatabase $db
+ * @param Title $title
+ * @param string $timestamp
+ * @return RevisionRecord|null
+ */
+ public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
+ 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)
+ * @param Title|null $title
+ *
+ * @return RevisionRecord|null
+ */
+ private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
+ $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
+
+ $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 )
+ && $lb->getServerCount() > 1
+ && $lb->hasOrMadeRecentMasterChanges()
+ ) {
+ $flags = self::READ_LATEST;
+ $dbw = $this->getDBConnection( DB_MASTER );
+ $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
+ $this->releaseDBConnection( $dbw );
+ }
+
+ 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)
+ * @param Title|null $title
+ *
+ * @return RevisionRecord|null
+ */
+ private function loadRevisionFromConds(
+ IDatabase $db,
+ $conditions,
+ $flags = 0,
+ Title $title = null
+ ) {
+ $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
+ 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
+ */
+ private function checkDatabaseWikiId( IDatabase $db ) {
+ $storeWiki = $this->wikiId;
+ $dbWiki = $db->getDomainID();
+
+ if ( $dbWiki === $storeWiki ) {
+ return;
+ }
+
+ // XXX: we really want the default database ID...
+ $storeWiki = $storeWiki ?: wfWikiID();
+ $dbWiki = $dbWiki ?: wfWikiID();
+
+ if ( $dbWiki === $storeWiki ) {
+ return;
+ }
+
+ // HACK: counteract encoding imposed by DatabaseDomain
+ $storeWiki = str_replace( '?h', '-', $storeWiki );
+ $dbWiki = str_replace( '?h', '-', $dbWiki );
+
+ if ( $dbWiki === $storeWiki ) {
+ return;
+ }
+
+ throw new MWException( "RevisionStore for $storeWiki "
+ . "cannot be used with a DB connection for $dbWiki" );
+ }
+
+ /**
+ * 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)
+ *
+ * @return object|false data row as a raw object
+ */
+ private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
+ $this->checkDatabaseWikiId( $db );
+
+ $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
+ $options = [];
+ if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
+ $options[] = 'FOR UPDATE';
+ }
+ return $db->selectRow(
+ $revQuery['tables'],
+ $revQuery['fields'],
+ $conditions,
+ __METHOD__,
+ $options,
+ $revQuery['joins']
+ );
+ }
+
+ /**
+ * Finds the ID of a content row for a given revision and slot role.
+ * This can be used to re-use content rows even while the content ID
+ * is still missing from SlotRecords, when writing to both the old and
+ * the new schema during MCR schema migration.
+ *
+ * @todo remove after MCR schema migration is complete.
+ *
+ * @param IDatabase $db
+ * @param int $revId
+ * @param string $role
+ *
+ * @return int|null
+ */
+ private function findSlotContentId( IDatabase $db, $revId, $role ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+ return null;
+ }
+
+ try {
+ $roleId = $this->slotRoleStore->getId( $role );
+ $conditions = [
+ 'slot_revision_id' => $revId,
+ 'slot_role_id' => $roleId,
+ ];
+
+ $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
+
+ return $contentId ?: null;
+ } catch ( NameTableAccessException $ex ) {
+ // If the role is missing from the slot_roles table,
+ // the corresponding row in slots cannot exist.
+ return null;
+ }
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new RevisionStoreRecord object.
+ *
+ * MCR migration note: this replaces Revision::getQueryInfo
+ *
+ * If the format of fields returned changes in any way then the cache key provided by
+ * self::getRevisionRowCacheKey should be updated.
+ *
+ * @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
+ * - 'text': Join with the text table, and select fields to load page text. This
+ * option is deprecated in MW 1.32 when the MCR migration flag SCHEMA_COMPAT_WRITE_NEW
+ * is set, and disallowed when SCHEMA_COMPAT_READ_OLD is not set.
+ *
+ * @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 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',
+ ] );
+
+ $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
+ $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
+ $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
+ $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
+
+ $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
+ $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
+ $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
+ $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
+
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ $ret['fields'][] = 'rev_text_id';
+
+ if ( $this->contentHandlerUseDB ) {
+ $ret['fields'][] = 'rev_content_format';
+ $ret['fields'][] = 'rev_content_model';
+ }
+ }
+
+ 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',
+ ] );
+ $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
+ }
+
+ if ( in_array( 'user', $options, true ) ) {
+ $ret['tables'][] = 'user';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'user_name',
+ ] );
+ $u = $actorQuery['fields']['rev_user'];
+ $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
+ }
+
+ if ( in_array( 'text', $options, true ) ) {
+ if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
+ throw new InvalidArgumentException( 'text table can no longer be joined directly' );
+ } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ // NOTE: even when this class is set to not read from the old schema, callers
+ // should still be able to join against the text table, as long as we are still
+ // writing the old schema for compatibility.
+ // TODO: This should trigger a deprecation warning eventually (T200918), but not
+ // before all known usages are removed (see T198341 and T201164).
+ // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
+ }
+
+ $ret['tables'][] = 'text';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'old_text',
+ 'old_flags'
+ ] );
+ $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * 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
+ * - '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
+ *
+ * @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 getSlotsQueryInfo( $options = [] ) {
+ $ret = [
+ 'tables' => [],
+ 'fields' => [],
+ 'joins' => [],
+ ];
+
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ $db = $this->getDBConnectionRef( DB_REPLICA );
+ $ret['tables']['slots'] = 'revision';
+
+ $ret['fields']['slot_revision_id'] = 'slots.rev_id';
+ $ret['fields']['slot_content_id'] = 'NULL';
+ $ret['fields']['slot_origin'] = 'slots.rev_id';
+ $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
+
+ if ( in_array( 'content', $options, true ) ) {
+ $ret['fields']['content_size'] = 'slots.rev_len';
+ $ret['fields']['content_sha1'] = 'slots.rev_sha1';
+ $ret['fields']['content_address']
+ = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
+
+ if ( $this->contentHandlerUseDB ) {
+ $ret['fields']['model_name'] = 'slots.rev_content_model';
+ } else {
+ $ret['fields']['model_name'] = 'NULL';
+ }
+ }
+ } else {
+ $ret['tables'][] = 'slots';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'slot_role_id',
+ ] );
+
+ 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';
+ }
+
+ if ( in_array( 'content', $options, true ) ) {
+ $ret['tables'][] = 'content';
+ $ret['fields'] = array_merge( $ret['fields'], [
+ 'content_size',
+ 'content_sha1',
+ 'content_address',
+ 'content_model',
+ ] );
+ $ret['joins']['content'] = [ 'INNER 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';
+ }
+
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Return the tables, fields, and join conditions to be selected to create
+ * a new RevisionArchiveRecord object.
+ *
+ * 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() {
+ $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
+ $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
+ $ret = [
+ 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
+ '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',
+ ] + $commentQuery['fields'] + $actorQuery['fields'],
+ 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
+ ];
+
+ if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+ $ret['fields'][] = 'ar_text_id';
+
+ if ( $this->contentHandlerUseDB ) {
+ $ret['fields'][] = 'ar_content_format';
+ $ret['fields'][] = 'ar_content_model';
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Do a batched query for the sizes of a set of revisions.
+ *
+ * MCR migration note: this replaces Revision::getParentLengths
+ *
+ * @param int[] $revIds
+ * @return int[] associative array mapping revision IDs from $revIds to the nominal size
+ * of the corresponding revision.
+ */
+ public function getRevisionSizes( array $revIds ) {
+ return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
+ }
+
+ /**
+ * Do a batched query for the sizes of a set of revisions.
+ *
+ * MCR migration note: this replaces Revision::getParentLengths
+ *
+ * @deprecated use RevisionStore::getRevisionSizes instead.
+ *
+ * @param IDatabase $db
+ * @param int[] $revIds
+ * @return int[] associative array mapping revision IDs from $revIds to the nominal size
+ * of the corresponding revision.
+ */
+ public function listRevisionSizes( IDatabase $db, array $revIds ) {
+ $this->checkDatabaseWikiId( $db );
+
+ $revLens = [];
+ if ( !$revIds ) {
+ return $revLens; // empty
+ }
+
+ $res = $db->select(
+ 'revision',
+ [ 'rev_id', 'rev_len' ],
+ [ 'rev_id' => $revIds ],
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ $revLens[$row->rev_id] = intval( $row->rev_len );
+ }
+
+ return $revLens;
+ }
+
+ /**
+ * Get previous revision for this title
+ *
+ * MCR migration note: this replaces Revision::getPrevious
+ *
+ * @param RevisionRecord $rev
+ * @param Title|null $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
+ if ( $title === null ) {
+ $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
+ }
+ $prev = $title->getPreviousRevisionID( $rev->getId() );
+ if ( $prev ) {
+ return $this->getRevisionByTitle( $title, $prev );
+ }
+ return null;
+ }
+
+ /**
+ * Get next revision for this title
+ *
+ * MCR migration note: this replaces Revision::getNext
+ *
+ * @param RevisionRecord $rev
+ * @param Title|null $title if known (optional)
+ *
+ * @return RevisionRecord|null
+ */
+ public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
+ if ( $title === null ) {
+ $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
+ }
+ $next = $title->getNextRevisionID( $rev->getId() );
+ if ( $next ) {
+ return $this->getRevisionByTitle( $title, $next );
+ }
+ return null;
+ }
+
+ /**
+ * 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 ) {
+ $this->checkDatabaseWikiId( $db );
+
+ 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 );
+ }
+
+ /**
+ * Get rev_timestamp from rev_id, without loading the rest of the row
+ *
+ * MCR migration note: this replaces Revision::getTimestampFromId
+ *
+ * @param Title $title
+ * @param int $id
+ * @param int $flags
+ * @return string|bool False if not found
+ */
+ public function getTimestampFromId( $title, $id, $flags = 0 ) {
+ $db = $this->getDBConnectionRefForQueryFlags( $flags );
+
+ $conds = [ 'rev_id' => $id ];
+ $conds['rev_page'] = $title->getArticleID();
+ $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
+
+ return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
+ }
+
+ /**
+ * 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 ) {
+ $this->checkDatabaseWikiId( $db );
+
+ $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 ) {
+ $this->checkDatabaseWikiId( $db );
+
+ if ( !$userId ) {
+ return false;
+ }
+
+ $revQuery = $this->getQueryInfo();
+ $res = $db->select(
+ $revQuery['tables'],
+ [
+ 'rev_user' => $revQuery['fields']['rev_user'],
+ ],
+ [
+ 'rev_page' => $pageId,
+ 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
+ $revQuery['joins']
+ );
+ 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
+ */
+ public function getKnownCurrentRevision( Title $title, $revId ) {
+ $db = $this->getDBConnectionRef( DB_REPLICA );
+
+ $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;
+ }
+
+ $row = $this->cache->getWithSetCallback(
+ // Page/rev IDs passed in from DB to reflect history merges
+ $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
+ WANObjectCache::TTL_WEEK,
+ function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
+ $setOpts += Database::getCacheSetOptions( $db );
+
+ $conds = [
+ 'rev_page' => intval( $pageId ),
+ 'page_id' => intval( $pageId ),
+ 'rev_id' => intval( $revId ),
+ ];
+
+ $row = $this->fetchRevisionRowFromConds( $db, $conds );
+ return $row ?: false; // don't cache negatives
+ }
+ );
+
+ // Reflect revision deletion and user renames
+ if ( $row ) {
+ return $this->newRevisionFromRow( $row, 0, $title );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * 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
+ );
+ }
+
+ // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionStore::class, 'MediaWiki\Storage\RevisionStore' );
--- /dev/null
+<?php
+
+/**
+ * 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
+ */
+
+namespace MediaWiki\Revision;
+
+use ActorMigration;
+use CommentStore;
+use MediaWiki\Logger\Spi as LoggerSpi;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStoreFactory;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\ILBFactory;
+
+/**
+ * Factory service for RevisionStore instances. This allows RevisionStores to be created for
+ * cross-wiki access.
+ *
+ * @warning Beware compatibility issues with schema migration in the context of cross-wiki access!
+ * This class assumes that all wikis are at compatible migration stages for all relevant schemas.
+ * Relevant schemas are: revision storage (MCR), the revision comment table, and the actor table.
+ * Migration stages are compatible as long as a) there are no wikis in the cluster that only write
+ * the old schema or b) there are no wikis that read only the new schema.
+ *
+ * @since 1.32
+ */
+class RevisionStoreFactory {
+
+ /** @var BlobStoreFactory */
+ private $blobStoreFactory;
+ /** @var ILBFactory */
+ private $dbLoadBalancerFactory;
+ /** @var WANObjectCache */
+ private $cache;
+ /** @var LoggerSpi */
+ private $loggerProvider;
+
+ /** @var CommentStore */
+ private $commentStore;
+ /** @var ActorMigration */
+ private $actorMigration;
+ /** @var int One of the MIGRATION_* constants */
+ private $mcrMigrationStage;
+ /**
+ * @var bool
+ * @see $wgContentHandlerUseDB
+ */
+ private $contentHandlerUseDB;
+
+ /** @var NameTableStoreFactory */
+ private $nameTables;
+
+ /**
+ * @param ILBFactory $dbLoadBalancerFactory
+ * @param BlobStoreFactory $blobStoreFactory
+ * @param NameTableStoreFactory $nameTables
+ * @param WANObjectCache $cache
+ * @param CommentStore $commentStore
+ * @param ActorMigration $actorMigration
+ * @param int $migrationStage
+ * @param LoggerSpi $loggerProvider
+ * @param bool $contentHandlerUseDB see {@link $wgContentHandlerUseDB}. Must be the same
+ * for all wikis in the cluster. Will go away after MCR migration.
+ */
+ public function __construct(
+ ILBFactory $dbLoadBalancerFactory,
+ BlobStoreFactory $blobStoreFactory,
+ NameTableStoreFactory $nameTables,
+ WANObjectCache $cache,
+ CommentStore $commentStore,
+ ActorMigration $actorMigration,
+ $migrationStage,
+ LoggerSpi $loggerProvider,
+ $contentHandlerUseDB
+ ) {
+ Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
+ $this->dbLoadBalancerFactory = $dbLoadBalancerFactory;
+ $this->blobStoreFactory = $blobStoreFactory;
+ $this->nameTables = $nameTables;
+ $this->cache = $cache;
+ $this->commentStore = $commentStore;
+ $this->actorMigration = $actorMigration;
+ $this->mcrMigrationStage = $migrationStage;
+ $this->loggerProvider = $loggerProvider;
+ $this->contentHandlerUseDB = $contentHandlerUseDB;
+ }
+
+ /**
+ * @since 1.32
+ *
+ * @param bool|string $wikiId false for the current domain / wikid
+ *
+ * @return RevisionStore for the given wikiId with all necessary services and a logger
+ */
+ public function getRevisionStore( $wikiId = false ) {
+ Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+ $store = new RevisionStore(
+ $this->dbLoadBalancerFactory->getMainLB( $wikiId ),
+ $this->blobStoreFactory->newSqlBlobStore( $wikiId ),
+ $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore.
+ $this->commentStore,
+ $this->nameTables->getContentModels( $wikiId ),
+ $this->nameTables->getSlotRoles( $wikiId ),
+ $this->mcrMigrationStage,
+ $this->actorMigration,
+ $wikiId
+ );
+
+ $store->setLogger( $this->loggerProvider->getLogger( 'RevisionStore' ) );
+ $store->setContentHandlerUseDB( $this->contentHandlerUseDB );
+
+ return $store;
+ }
+}
--- /dev/null
+<?php
+/**
+ * A RevisionRecord representing an existing revision persisted in the revision table.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\User\UserIdentity;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A RevisionRecord representing an existing revision persisted in the revision table.
+ * RevisionStoreRecord has no optional fields, getters will never return null.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\RevisionStoreRecord
+ */
+class RevisionStoreRecord extends RevisionRecord {
+
+ /** @var bool */
+ protected $mCurrent = false;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
+ * a query that yields the required fields.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ */
+ function __construct(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ parent::__construct( $title, $slots, $wikiId );
+ Assert::parameterType( 'object', $row, '$row' );
+
+ $this->mId = intval( $row->rev_id );
+ $this->mPageId = intval( $row->rev_page );
+ $this->mComment = $comment;
+
+ $timestamp = wfTimestamp( TS_MW, $row->rev_timestamp );
+ Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
+
+ $this->mUser = $user;
+ $this->mMinorEdit = boolval( $row->rev_minor_edit );
+ $this->mTimestamp = $timestamp;
+ $this->mDeleted = intval( $row->rev_deleted );
+
+ // NOTE: rev_parent_id = 0 indicates that there is no parent revision, while null
+ // indicates that the parent revision is unknown. As per MW 1.31, the database schema
+ // allows rev_parent_id to be NULL.
+ $this->mParentId = isset( $row->rev_parent_id ) ? intval( $row->rev_parent_id ) : null;
+ $this->mSize = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
+ $this->mSha1 = !empty( $row->rev_sha1 ) ? $row->rev_sha1 : null;
+
+ // NOTE: we must not call $this->mTitle->getLatestRevID() here, since the state of
+ // page_latest may be in limbo during revision creation. In that case, calling
+ // $this->mTitle->getLatestRevID() would cause a bad value to be cached in the Title
+ // object. During page creation, that bad value would be 0.
+ if ( isset( $row->page_latest ) ) {
+ $this->mCurrent = ( $row->rev_id == $row->page_latest );
+ }
+
+ // sanity check
+ if (
+ $this->mPageId && $this->mTitle->exists()
+ && $this->mPageId !== $this->mTitle->getArticleID()
+ ) {
+ throw new InvalidArgumentException(
+ 'The given Title does not belong to page ID ' . $this->mPageId .
+ ' but actually belongs to ' . $this->mTitle->getArticleID()
+ );
+ }
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isCurrent
+ *
+ * @return bool
+ */
+ public function isCurrent() {
+ return $this->mCurrent;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isDeleted
+ *
+ * @param int $field One of DELETED_* bitfield constants
+ *
+ * @return bool
+ */
+ public function isDeleted( $field ) {
+ if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
+ // Current revisions of pages cannot have the content hidden. Skipping this
+ // check is very useful for Parser as it fetches templates using newKnownCurrent().
+ // Calling getVisibility() in that case triggers a verification database query.
+ return false; // no need to check
+ }
+
+ return parent::isDeleted( $field );
+ }
+
+ protected function userCan( $field, User $user ) {
+ if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
+ // Current revisions of pages cannot have the content hidden. Skipping this
+ // check is very useful for Parser as it fetches templates using newKnownCurrent().
+ // Calling getVisibility() in that case triggers a verification database query.
+ return true; // no need to check
+ }
+
+ return parent::userCan( $field, $user );
+ }
+
+ /**
+ * @return int The revision id, never null.
+ */
+ public function getId() {
+ // overwritten just to add a guarantee to the contract
+ return parent::getId();
+ }
+
+ /**
+ * @throws RevisionAccessException if the size was unknown and could not be calculated.
+ * @return string The nominal revision size, never null. May be computed on the fly.
+ */
+ public function getSize() {
+ // If length is null, calculate and remember it (potentially SLOW!).
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * @throws RevisionAccessException if the hash was unknown and could not be calculated.
+ * @return string The revision hash, never null. May be computed on the fly.
+ */
+ public function getSha1() {
+ // If hash is null, calculate it and remember (potentially SLOW!)
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return UserIdentity The identity of the revision author, null if access is forbidden.
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getUser( $audience, $user );
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return CommentStoreComment The revision comment, null if access is forbidden.
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getComment( $audience, $user );
+ }
+
+ /**
+ * @return string timestamp, never null
+ */
+ public function getTimestamp() {
+ // overwritten just to add a guarantee to the contract
+ return parent::getTimestamp();
+ }
+
+ /**
+ * @see RevisionStore::isComplete
+ *
+ * @return bool always true.
+ */
+ public function isReadyForInsertion() {
+ return true;
+ }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( RevisionStoreRecord::class, 'MediaWiki\Storage\RevisionStoreRecord' );
--- /dev/null
+<?php
+/**
+ * Value object representing a content slot associated with a page revision.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+use Content;
+use InvalidArgumentException;
+use LogicException;
+use OutOfBoundsException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing a content slot associated with a page revision.
+ * SlotRecord provides direct access to a Content object.
+ * That access may be implemented through a callback.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\SlotRecord
+ */
+class SlotRecord {
+
+ const MAIN = 'main';
+
+ /**
+ * @var object database result row, as a raw object. Callbacks are supported for field values,
+ * to enable on-demand emulation of these values. This is primarily intended for use
+ * during schema migration.
+ */
+ private $row;
+
+ /**
+ * @var Content|callable
+ */
+ private $content;
+
+ /**
+ * Returns a new SlotRecord just like the given $slot, except that calling getContent()
+ * will fail with an exception.
+ *
+ * @param SlotRecord $slot
+ *
+ * @return SlotRecord
+ */
+ public static function newWithSuppressedContent( SlotRecord $slot ) {
+ $row = $slot->row;
+
+ return new SlotRecord( $row, function () {
+ throw new SuppressedDataException( 'Content suppressed!' );
+ } );
+ }
+
+ /**
+ * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
+ * The slot's content cannot be overwritten.
+ *
+ * @param SlotRecord $slot
+ * @param array $overrides
+ *
+ * @return SlotRecord
+ */
+ private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
+ $row = clone $slot->row;
+ $row->slot_id = null; // never copy the row ID!
+
+ foreach ( $overrides as $key => $value ) {
+ $row->$key = $value;
+ }
+
+ return new SlotRecord( $row, $slot->content );
+ }
+
+ /**
+ * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
+ * of a previous revision.
+ *
+ * Note that a SlotRecord constructed this way are intended as prototypes,
+ * to be used wit newSaved(). They are incomplete, so some getters such as
+ * getRevision() will fail.
+ *
+ * @param SlotRecord $slot
+ *
+ * @return SlotRecord
+ */
+ public static function newInherited( SlotRecord $slot ) {
+ // Sanity check - we can't inherit from a Slot that's not attached to a revision.
+ $slot->getRevision();
+ $slot->getOrigin();
+ $slot->getAddress();
+
+ // NOTE: slot_origin and content_address are copied from $slot.
+ return self::newDerived( $slot, [
+ 'slot_revision_id' => null,
+ ] );
+ }
+
+ /**
+ * Constructs a new Slot from a Content object for a new revision.
+ * This is the preferred way to construct a slot for storing Content that
+ * resulted from a user edit. The slot is assumed to be not inherited.
+ *
+ * Note that a SlotRecord constructed this way are intended as prototypes,
+ * to be used wit newSaved(). They are incomplete, so some getters such as
+ * getAddress() will fail.
+ *
+ * @param string $role
+ * @param Content $content
+ *
+ * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
+ */
+ public static function newUnsaved( $role, Content $content ) {
+ Assert::parameterType( 'string', $role, '$role' );
+
+ $row = [
+ 'slot_id' => null, // not yet known
+ 'slot_revision_id' => null, // not yet known
+ 'slot_origin' => null, // not yet known, will be set in newSaved()
+ 'content_size' => null, // compute later
+ 'content_sha1' => null, // compute later
+ 'slot_content_id' => null, // not yet known, will be set in newSaved()
+ 'content_address' => null, // not yet known, will be set in newSaved()
+ 'role_name' => $role,
+ 'model_name' => $content->getModel(),
+ ];
+
+ return new SlotRecord( (object)$row, $content );
+ }
+
+ /**
+ * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
+ * proto-slot. This adds information that has only become available during saving,
+ * particularly the revision ID, content ID and content address.
+ *
+ * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
+ * If $protoSlot already has a revision, it must be the same.
+ * @param int|null $contentId the ID of the row in the content table describing the content
+ * referenced by $contentAddress (field slot_content_id).
+ * If $protoSlot already has a content ID, it must be the same.
+ * @param string $contentAddress the slot's content address (field content_address).
+ * If $protoSlot already has an address, it must be the same.
+ * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new
+ * revision. $protoSlot must have a content address if inherited.
+ *
+ * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision.
+ */
+ public static function newSaved(
+ $revisionId,
+ $contentId,
+ $contentAddress,
+ SlotRecord $protoSlot
+ ) {
+ Assert::parameterType( 'integer', $revisionId, '$revisionId' );
+ // TODO once migration is over $contentId must be an integer
+ Assert::parameterType( 'integer|null', $contentId, '$contentId' );
+ Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
+
+ if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
+ throw new LogicException(
+ "Mismatching revision ID $revisionId: "
+ . "The slot already belongs to revision {$protoSlot->getRevision()}. "
+ . "Use SlotRecord::newInherited() to re-use content between revisions."
+ );
+ }
+
+ if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
+ throw new LogicException(
+ "Mismatching blob address $contentAddress: "
+ . "The slot already has content at {$protoSlot->getAddress()}."
+ );
+ }
+
+ if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
+ throw new LogicException(
+ "Mismatching content ID $contentId: "
+ . "The slot already has content row {$protoSlot->getContentId()} associated."
+ );
+ }
+
+ if ( $protoSlot->isInherited() ) {
+ if ( !$protoSlot->hasAddress() ) {
+ throw new InvalidArgumentException(
+ "An inherited blob should have a content address!"
+ );
+ }
+ if ( !$protoSlot->hasField( 'slot_origin' ) ) {
+ throw new InvalidArgumentException(
+ "A saved inherited slot should have an origin set!"
+ );
+ }
+ $origin = $protoSlot->getOrigin();
+ } else {
+ $origin = $revisionId;
+ }
+
+ return self::newDerived( $protoSlot, [
+ 'slot_revision_id' => $revisionId,
+ 'slot_content_id' => $contentId,
+ 'slot_origin' => $origin,
+ 'content_address' => $contentAddress,
+ ] );
+ }
+
+ /**
+ * SlotRecord constructor.
+ *
+ * The following fields are supported by the $row parameter:
+ *
+ * $row->blob_data
+ * $row->blob_address
+ *
+ * @param object $row A database row composed of fields of the slot and content tables,
+ * as a raw object. Any field value can be a callback that produces the field value
+ * given this SlotRecord as a parameter. However, plain strings cannot be used as
+ * callbacks here, for security reasons.
+ * @param Content|callable $content The content object associated with the slot, or a
+ * callback that will return that Content object, given this SlotRecord as a parameter.
+ */
+ public function __construct( $row, $content ) {
+ Assert::parameterType( 'object', $row, '$row' );
+ Assert::parameterType( 'Content|callable', $content, '$content' );
+
+ Assert::parameter(
+ property_exists( $row, 'slot_revision_id' ),
+ '$row->slot_revision_id',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'slot_content_id' ),
+ '$row->slot_content_id',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'content_address' ),
+ '$row->content_address',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'model_name' ),
+ '$row->model_name',
+ 'must exist'
+ );
+ Assert::parameter(
+ property_exists( $row, 'slot_origin' ),
+ '$row->slot_origin',
+ 'must exist'
+ );
+ Assert::parameter(
+ !property_exists( $row, 'slot_inherited' ),
+ '$row->slot_inherited',
+ 'must not exist'
+ );
+ Assert::parameter(
+ !property_exists( $row, 'slot_revision' ),
+ '$row->slot_revision',
+ 'must not exist'
+ );
+
+ $this->row = $row;
+ $this->content = $content;
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * Returns the Content of the given slot.
+ *
+ * @note This is free to load Content from whatever subsystem is necessary,
+ * performing potentially expensive operations and triggering I/O-related
+ * failure modes.
+ *
+ * @note This method does not apply audience filtering.
+ *
+ * @throws SuppressedDataException if access to the content is not allowed according
+ * to the audience check performed by RevisionRecord::getSlot().
+ *
+ * @return Content The slot's content. This is a direct reference to the internal instance,
+ * copy before exposing to application logic!
+ */
+ public function getContent() {
+ if ( $this->content instanceof Content ) {
+ return $this->content;
+ }
+
+ $obj = call_user_func( $this->content, $this );
+
+ Assert::postcondition(
+ $obj instanceof Content,
+ 'Slot content callback should return a Content object'
+ );
+
+ $this->content = $obj;
+
+ return $this->content;
+ }
+
+ /**
+ * Returns the string value of a data field from the database row supplied to the constructor.
+ * If the field was set to a callback, that callback is invoked and the result returned.
+ *
+ * @param string $name
+ *
+ * @throws OutOfBoundsException
+ * @throws IncompleteRevisionException
+ * @return mixed Returns the field's value, never null.
+ */
+ private function getField( $name ) {
+ if ( !isset( $this->row->$name ) ) {
+ // distinguish between unknown and uninitialized fields
+ if ( property_exists( $this->row, $name ) ) {
+ throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
+ } else {
+ throw new OutOfBoundsException( 'No such field: ' . $name );
+ }
+ }
+
+ $value = $this->row->$name;
+
+ // NOTE: allow callbacks, but don't trust plain string callables from the database!
+ if ( !is_string( $value ) && is_callable( $value ) ) {
+ $value = call_user_func( $value, $this );
+ $this->setField( $name, $value );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns the string value of a data field from the database row supplied to the constructor.
+ *
+ * @param string $name
+ *
+ * @throws OutOfBoundsException
+ * @throws IncompleteRevisionException
+ * @return string Returns the string value
+ */
+ private function getStringField( $name ) {
+ return strval( $this->getField( $name ) );
+ }
+
+ /**
+ * Returns the int value of a data field from the database row supplied to the constructor.
+ *
+ * @param string $name
+ *
+ * @throws OutOfBoundsException
+ * @throws IncompleteRevisionException
+ * @return int Returns the int value
+ */
+ private function getIntField( $name ) {
+ return intval( $this->getField( $name ) );
+ }
+
+ /**
+ * @param string $name
+ * @return bool whether this record contains the given field
+ */
+ private function hasField( $name ) {
+ if ( isset( $this->row->$name ) ) {
+ // if the field is a callback, resolve first, then re-check
+ if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
+ $this->getField( $name );
+ }
+ }
+
+ return isset( $this->row->$name );
+ }
+
+ /**
+ * Returns the ID of the revision this slot is associated with.
+ *
+ * @return int
+ */
+ public function getRevision() {
+ return $this->getIntField( 'slot_revision_id' );
+ }
+
+ /**
+ * Returns the revision ID of the revision that originated the slot's content.
+ *
+ * @return int
+ */
+ public function getOrigin() {
+ return $this->getIntField( 'slot_origin' );
+ }
+
+ /**
+ * Whether this slot was inherited from an older revision.
+ *
+ * If this SlotRecord is already attached to a revision, this returns true
+ * if the slot's revision of origin is the same as the revision it belongs to.
+ *
+ * If this SlotRecord is not yet attached to a revision, this returns true
+ * if the slot already has an address.
+ *
+ * @return bool
+ */
+ public function isInherited() {
+ if ( $this->hasRevision() ) {
+ return $this->getRevision() !== $this->getOrigin();
+ } else {
+ return $this->hasAddress();
+ }
+ }
+
+ /**
+ * Whether this slot has an address. Slots will have an address if their
+ * content has been stored. While building a new revision,
+ * SlotRecords will not have an address associated.
+ *
+ * @return bool
+ */
+ public function hasAddress() {
+ return $this->hasField( 'content_address' );
+ }
+
+ /**
+ * Whether this slot has an origin (revision ID that originated the slot's content.
+ *
+ * @since 1.32
+ *
+ * @return bool
+ */
+ public function hasOrigin() {
+ return $this->hasField( 'slot_origin' );
+ }
+
+ /**
+ * Whether this slot has a content ID. Slots will have a content ID if their
+ * content has been stored in the content table. While building a new revision,
+ * SlotRecords will not have an ID associated.
+ *
+ * Also, during schema migration, hasContentId() may return false when encountering an
+ * un-migrated database entry in SCHEMA_COMPAT_WRITE_BOTH mode.
+ * It will however always return true for saved revisions on SCHEMA_COMPAT_READ_NEW mode,
+ * or without SCHEMA_COMPAT_WRITE_NEW mode. In the latter case, an emulated content ID
+ * is used, derived from the revision's text ID.
+ *
+ * Note that hasContentId() returning false while hasRevision() returns true always
+ * indicates an unmigrated row in SCHEMA_COMPAT_WRITE_BOTH mode, as described above.
+ * For an unsaved slot, both these methods would return false.
+ *
+ * @since 1.32
+ *
+ * @return bool
+ */
+ public function hasContentId() {
+ return $this->hasField( 'slot_content_id' );
+ }
+
+ /**
+ * Whether this slot has revision ID associated. Slots will have a revision ID associated
+ * only if they were loaded as part of an existing revision. While building a new revision,
+ * Slotrecords will not have a revision ID associated.
+ *
+ * @return bool
+ */
+ public function hasRevision() {
+ return $this->hasField( 'slot_revision_id' );
+ }
+
+ /**
+ * Returns the role of the slot.
+ *
+ * @return string
+ */
+ public function getRole() {
+ return $this->getStringField( 'role_name' );
+ }
+
+ /**
+ * Returns the address of this slot's content.
+ * This address can be used with BlobStore to load the Content object.
+ *
+ * @return string
+ */
+ public function getAddress() {
+ return $this->getStringField( 'content_address' );
+ }
+
+ /**
+ * Returns the ID of the content meta data row associated with the slot.
+ * This information should be irrelevant to application logic, it is here to allow
+ * the construction of a full row for the revision table.
+ *
+ * Note that this method may return an emulated value during schema migration in
+ * SCHEMA_COMPAT_WRITE_OLD mode. See RevisionStore::emulateContentId for more information.
+ *
+ * @return int
+ */
+ public function getContentId() {
+ return $this->getIntField( 'slot_content_id' );
+ }
+
+ /**
+ * Returns the content size
+ *
+ * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
+ */
+ public function getSize() {
+ try {
+ $size = $this->getIntField( 'content_size' );
+ } catch ( IncompleteRevisionException $ex ) {
+ $size = $this->getContent()->getSize();
+ $this->setField( 'content_size', $size );
+ }
+
+ return $size;
+ }
+
+ /**
+ * Returns the content size
+ *
+ * @return string hash of the content.
+ */
+ public function getSha1() {
+ try {
+ $sha1 = $this->getStringField( 'content_sha1' );
+ } catch ( IncompleteRevisionException $ex ) {
+ $format = $this->hasField( 'format_name' )
+ ? $this->getStringField( 'format_name' )
+ : null;
+
+ $data = $this->getContent()->serialize( $format );
+ $sha1 = self::base36Sha1( $data );
+ $this->setField( 'content_sha1', $sha1 );
+ }
+
+ return $sha1;
+ }
+
+ /**
+ * Returns the content model. This is the model name that decides
+ * which ContentHandler is appropriate for interpreting the
+ * data of the blob referenced by the address returned by getAddress().
+ *
+ * @return string the content model of the content
+ */
+ public function getModel() {
+ try {
+ $model = $this->getStringField( 'model_name' );
+ } catch ( IncompleteRevisionException $ex ) {
+ $model = $this->getContent()->getModel();
+ $this->setField( 'model_name', $model );
+ }
+
+ return $model;
+ }
+
+ /**
+ * Returns the blob serialization format as a MIME type.
+ *
+ * @note When this method returns null, the caller is expected
+ * to auto-detect the serialization format, or to rely on
+ * the default format associated with the content model.
+ *
+ * @return string|null
+ */
+ public function getFormat() {
+ // XXX: we currently do not plan to store the format for each slot!
+
+ if ( $this->hasField( 'format_name' ) ) {
+ return $this->getStringField( 'format_name' );
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $name
+ * @param string|int|null $value
+ */
+ private function setField( $name, $value ) {
+ $this->row->$name = $value;
+ }
+
+ /**
+ * Get the base 36 SHA-1 value for a string of text
+ *
+ * MCR migration note: this replaces Revision::base36Sha1
+ *
+ * @param string $blob
+ * @return string
+ */
+ public static function base36Sha1( $blob ) {
+ return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
+ }
+
+ /**
+ * Returns true if $other has the same content as this slot.
+ * The check is performed based on the model, address size, and hash.
+ * Two slots can have the same content if they use different content addresses,
+ * but if they have the same address and the same model, they have the same content.
+ * Two slots can have the same content if they belong to different
+ * revisions or pages.
+ *
+ * Note that hasSameContent() may return false even if Content::equals returns true for
+ * the content of two slots. This may happen if the two slots have different serializations
+ * representing equivalent Content. Such false negatives are considered acceptable. Code
+ * that has to be absolutely sure the Content is really not the same if hasSameContent()
+ * returns false should call getContent() and compare the Content objects directly.
+ *
+ * @since 1.32
+ *
+ * @param SlotRecord $other
+ * @return bool
+ */
+ public function hasSameContent( SlotRecord $other ) {
+ if ( $other === $this ) {
+ return true;
+ }
+
+ if ( $this->getModel() !== $other->getModel() ) {
+ return false;
+ }
+
+ if ( $this->hasAddress()
+ && $other->hasAddress()
+ && $this->getAddress() == $other->getAddress()
+ ) {
+ return true;
+ }
+
+ if ( $this->getSize() !== $other->getSize() ) {
+ return false;
+ }
+
+ if ( $this->getSha1() !== $other->getSha1() ) {
+ return false;
+ }
+
+ return true;
+ }
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( SlotRecord::class, 'MediaWiki\Storage\SlotRecord' );
*/
namespace MediaWiki\Revision;
-use MediaWiki\Storage\SuppressedDataException;
use ParserOutput;
/**
--- /dev/null
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * 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
+ *
+ * @file
+ */
+
+namespace MediaWiki\Revision;
+
+/**
+ * Exception raised in response to an audience check when attempting to
+ * access suppressed information without permission.
+ *
+ * @since 1.31
+ * @since 1.32 Renamed from MediaWiki\Storage\SuppressedDataException
+ */
+class SuppressedDataException extends RevisionAccessException {
+
+}
+
+/**
+ * Retain the old class name for backwards compatibility.
+ * @deprecated since 1.32
+ */
+class_alias( SuppressedDataException::class, 'MediaWiki\Storage\SuppressedDataException' );
use MediaWiki\MediaWikiServices;
use MediaWiki\Preferences\PreferencesFactory;
use MediaWiki\Preferences\DefaultPreferencesFactory;
+use MediaWiki\Revision\RevisionFactory;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreFactory;
use MediaWiki\Shell\CommandFactory;
use MediaWiki\Special\SpecialPageFactory;
use MediaWiki\Storage\BlobStore;
-use MediaWiki\Revision\RevisionRenderer;
use MediaWiki\Storage\BlobStoreFactory;
use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\RevisionFactory;
-use MediaWiki\Storage\RevisionLookup;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreFactory;
use MediaWiki\Storage\SqlBlobStore;
return [
use LinksUpdate;
use LogicException;
use MediaWiki\Edit\PreparedEdit;
+use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RenderedRevision;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\User\UserIdentity;
use MessageCache;
use ParserCache;
+++ /dev/null
-<?php
-/**
- * Exception representing a failure to look up a revision.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-/**
- * Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
- *
- * @since 1.31
- */
-class IncompleteRevisionException extends RevisionAccessException {
-
-}
+++ /dev/null
-<?php
-/**
- * Mutable RevisionRecord implementation, for building new revision entries programmatically.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use Content;
-use InvalidArgumentException;
-use MediaWiki\User\UserIdentity;
-use MWException;
-use Title;
-use Wikimedia\Assert\Assert;
-
-/**
- * Mutable RevisionRecord implementation, for building new revision entries programmatically.
- * Provides setters for all fields.
- *
- * @since 1.31
- */
-class MutableRevisionRecord extends RevisionRecord {
-
- /**
- * Returns an incomplete MutableRevisionRecord which uses $parent as its
- * parent revision, and inherits all slots form it. If saved unchanged,
- * the new revision will act as a null-revision.
- *
- * @param RevisionRecord $parent
- *
- * @return MutableRevisionRecord
- */
- public static function newFromParentRevision( RevisionRecord $parent ) {
- // TODO: ideally, we wouldn't need a Title here
- $title = Title::newFromLinkTarget( $parent->getPageAsLinkTarget() );
- $rev = new MutableRevisionRecord( $title, $parent->getWikiId() );
-
- foreach ( $parent->getSlotRoles() as $role ) {
- $slot = $parent->getSlot( $role, self::RAW );
- $rev->inheritSlot( $slot );
- }
-
- $rev->setPageId( $parent->getPageId() );
- $rev->setParentId( $parent->getId() );
-
- return $rev;
- }
-
- /**
- * @note Avoid calling this constructor directly. Use the appropriate methods
- * in RevisionStore instead.
- *
- * @param Title $title The title of the page this Revision is associated with.
- * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
- * or false for the local site.
- *
- * @throws MWException
- */
- function __construct( Title $title, $wikiId = false ) {
- $slots = new MutableRevisionSlots();
-
- parent::__construct( $title, $slots, $wikiId );
-
- $this->mSlots = $slots; // redundant, but nice for static analysis
- }
-
- /**
- * @param int $parentId
- */
- public function setParentId( $parentId ) {
- Assert::parameterType( 'integer', $parentId, '$parentId' );
-
- $this->mParentId = $parentId;
- }
-
- /**
- * Sets the given slot. If a slot with the same role is already present in the revision,
- * it is replaced.
- *
- * @note This can only be used with a fresh "unattached" SlotRecord. Calling code that has a
- * SlotRecord from another revision should use inheritSlot(). Calling code that has access to
- * a Content object can use setContent().
- *
- * @note This may cause the slot meta-data for the revision to be lazy-loaded.
- *
- * @note Calling this method will cause the revision size and hash to be re-calculated upon
- * the next call to getSize() and getSha1(), respectively.
- *
- * @param SlotRecord $slot
- */
- public function setSlot( SlotRecord $slot ) {
- if ( $slot->hasRevision() && $slot->getRevision() !== $this->getId() ) {
- throw new InvalidArgumentException(
- 'The given slot must be an unsaved, unattached one. '
- . 'This slot is already attached to revision ' . $slot->getRevision() . '. '
- . 'Use inheritSlot() instead to preserve a slot from a previous revision.'
- );
- }
-
- $this->mSlots->setSlot( $slot );
- $this->resetAggregateValues();
- }
-
- /**
- * "Inherits" the given slot's content.
- *
- * If a slot with the same role is already present in the revision, it is replaced.
- *
- * @note This may cause the slot meta-data for the revision to be lazy-loaded.
- *
- * @param SlotRecord $parentSlot
- */
- public function inheritSlot( SlotRecord $parentSlot ) {
- $this->mSlots->inheritSlot( $parentSlot );
- $this->resetAggregateValues();
- }
-
- /**
- * Sets the content for the slot with the given role.
- *
- * If a slot with the same role is already present in the revision, it is replaced.
- * Calling code that has access to a SlotRecord can use inheritSlot() instead.
- *
- * @note This may cause the slot meta-data for the revision to be lazy-loaded.
- *
- * @note Calling this method will cause the revision size and hash to be re-calculated upon
- * the next call to getSize() and getSha1(), respectively.
- *
- * @param string $role
- * @param Content $content
- */
- public function setContent( $role, Content $content ) {
- $this->mSlots->setContent( $role, $content );
- $this->resetAggregateValues();
- }
-
- /**
- * Removes the slot with the given role from this revision.
- * This effectively ends the "stream" with that role on the revision's page.
- * Future revisions will no longer inherit this slot, unless it is added back explicitly.
- *
- * @note This may cause the slot meta-data for the revision to be lazy-loaded.
- *
- * @note Calling this method will cause the revision size and hash to be re-calculated upon
- * the next call to getSize() and getSha1(), respectively.
- *
- * @param string $role
- */
- public function removeSlot( $role ) {
- $this->mSlots->removeSlot( $role );
- $this->resetAggregateValues();
- }
-
- /**
- * Applies the given update to the slots of this revision.
- *
- * @param RevisionSlotsUpdate $update
- */
- public function applyUpdate( RevisionSlotsUpdate $update ) {
- $update->apply( $this->mSlots );
- }
-
- /**
- * @param CommentStoreComment $comment
- */
- public function setComment( CommentStoreComment $comment ) {
- $this->mComment = $comment;
- }
-
- /**
- * Set revision hash, for optimization. Prevents getSha1() from re-calculating the hash.
- *
- * @note This should only be used if the calling code is sure that the given hash is correct
- * for the revision's content, and there is no chance of the content being manipulated
- * later. When in doubt, this method should not be called.
- *
- * @param string $sha1 SHA1 hash as a base36 string.
- */
- public function setSha1( $sha1 ) {
- Assert::parameterType( 'string', $sha1, '$sha1' );
-
- $this->mSha1 = $sha1;
- }
-
- /**
- * Set nominal revision size, for optimization. Prevents getSize() from re-calculating the size.
- *
- * @note This should only be used if the calling code is sure that the given size is correct
- * for the revision's content, and there is no chance of the content being manipulated
- * later. When in doubt, this method should not be called.
- *
- * @param int $size nominal size in bogo-bytes
- */
- public function setSize( $size ) {
- Assert::parameterType( 'integer', $size, '$size' );
-
- $this->mSize = $size;
- }
-
- /**
- * @param int $visibility
- */
- public function setVisibility( $visibility ) {
- Assert::parameterType( 'integer', $visibility, '$visibility' );
-
- $this->mDeleted = $visibility;
- }
-
- /**
- * @param string $timestamp A timestamp understood by wfTimestamp
- */
- public function setTimestamp( $timestamp ) {
- Assert::parameterType( 'string', $timestamp, '$timestamp' );
-
- $this->mTimestamp = wfTimestamp( TS_MW, $timestamp );
- }
-
- /**
- * @param bool $minorEdit
- */
- public function setMinorEdit( $minorEdit ) {
- Assert::parameterType( 'boolean', $minorEdit, '$minorEdit' );
-
- $this->mMinorEdit = $minorEdit;
- }
-
- /**
- * Set the revision ID.
- *
- * MCR migration note: this replaces Revision::setId()
- *
- * @warning Use this with care, especially when preparing a revision for insertion
- * into the database! The revision ID should only be fixed in special cases
- * like preserving the original ID when restoring a revision.
- *
- * @param int $id
- */
- public function setId( $id ) {
- Assert::parameterType( 'integer', $id, '$id' );
-
- $this->mId = $id;
- }
-
- /**
- * Sets the user identity associated with the revision
- *
- * @param UserIdentity $user
- */
- public function setUser( UserIdentity $user ) {
- $this->mUser = $user;
- }
-
- /**
- * @param int $pageId
- */
- public function setPageId( $pageId ) {
- Assert::parameterType( 'integer', $pageId, '$pageId' );
-
- if ( $this->mTitle->exists() && $pageId !== $this->mTitle->getArticleID() ) {
- throw new InvalidArgumentException(
- 'The given Title does not belong to page ID ' . $this->mPageId
- );
- }
-
- $this->mPageId = $pageId;
- }
-
- /**
- * Returns the nominal size of this revision.
- *
- * MCR migration note: this replaces Revision::getSize
- *
- * @return int The nominal size, may be computed on the fly if not yet known.
- */
- public function getSize() {
- // If not known, re-calculate and remember. Will be reset when slots change.
- if ( $this->mSize === null ) {
- $this->mSize = $this->mSlots->computeSize();
- }
-
- return $this->mSize;
- }
-
- /**
- * Returns the base36 sha1 of this revision.
- *
- * MCR migration note: this replaces Revision::getSha1
- *
- * @return string The revision hash, may be computed on the fly if not yet known.
- */
- public function getSha1() {
- // If not known, re-calculate and remember. Will be reset when slots change.
- if ( $this->mSha1 === null ) {
- $this->mSha1 = $this->mSlots->computeSha1();
- }
-
- return $this->mSha1;
- }
-
- /**
- * Returns the slots defined for this revision as a MutableRevisionSlots instance,
- * which can be modified to defined the slots for this revision.
- *
- * @return MutableRevisionSlots
- */
- public function getSlots() {
- // Overwritten just guarantee the more narrow return type.
- return parent::getSlots();
- }
-
- /**
- * Invalidate cached aggregate values such as hash and size.
- */
- private function resetAggregateValues() {
- $this->mSize = null;
- $this->mSha1 = null;
- }
-
-}
+++ /dev/null
-<?php
-/**
- * Mutable version of RevisionSlots, for constructing a new revision.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use Content;
-
-/**
- * Mutable version of RevisionSlots, for constructing a new revision.
- *
- * @since 1.31
- */
-class MutableRevisionSlots extends RevisionSlots {
-
- /**
- * Constructs a MutableRevisionSlots that inherits from the given
- * list of slots.
- *
- * @param SlotRecord[] $slots
- *
- * @return MutableRevisionSlots
- */
- public static function newFromParentRevisionSlots( array $slots ) {
- $inherited = [];
- foreach ( $slots as $slot ) {
- $role = $slot->getRole();
- $inherited[$role] = SlotRecord::newInherited( $slot );
- }
-
- return new MutableRevisionSlots( $inherited );
- }
-
- /**
- * @param SlotRecord[] $slots An array of SlotRecords.
- */
- public function __construct( array $slots = [] ) {
- parent::__construct( $slots );
- }
-
- /**
- * Sets the given slot.
- * If a slot with the same role is already present, it is replaced.
- *
- * @param SlotRecord $slot
- */
- public function setSlot( SlotRecord $slot ) {
- if ( !is_array( $this->slots ) ) {
- $this->getSlots(); // initialize $this->slots
- }
-
- $role = $slot->getRole();
- $this->slots[$role] = $slot;
- }
-
- /**
- * Sets the given slot to an inherited version of $slot.
- * If a slot with the same role is already present, it is replaced.
- *
- * @param SlotRecord $slot
- */
- public function inheritSlot( SlotRecord $slot ) {
- $this->setSlot( SlotRecord::newInherited( $slot ) );
- }
-
- /**
- * Sets the content for the slot with the given role.
- * If a slot with the same role is already present, it is replaced.
- *
- * @param string $role
- * @param Content $content
- */
- public function setContent( $role, Content $content ) {
- $slot = SlotRecord::newUnsaved( $role, $content );
- $this->setSlot( $slot );
- }
-
- /**
- * Remove the slot for the given role, discontinue the corresponding stream.
- *
- * @param string $role
- */
- public function removeSlot( $role ) {
- if ( !is_array( $this->slots ) ) {
- $this->getSlots(); // initialize $this->slots
- }
-
- unset( $this->slots[$role] );
- }
-
-}
use LogicException;
use ManualLogEntry;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
use MWException;
use RecentChange;
use Revision;
+++ /dev/null
-<?php
-/**
- * Exception representing a failure to look up a revision.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use RuntimeException;
-
-/**
- * Exception representing a failure to look up a revision.
- *
- * @since 1.31
- */
-class RevisionAccessException extends RuntimeException {
-
-}
+++ /dev/null
-<?php
-/**
- * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use MediaWiki\User\UserIdentity;
-use Title;
-use User;
-use Wikimedia\Assert\Assert;
-
-/**
- * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
- * Most getters on RevisionArchiveRecord will never return null. However, getId() and
- * getParentId() may indeed return null if this information was not stored when the archive entry
- * was created.
- *
- * @since 1.31
- */
-class RevisionArchiveRecord extends RevisionRecord {
-
- /**
- * @var int
- */
- protected $mArchiveId;
-
- /**
- * @note Avoid calling this constructor directly. Use the appropriate methods
- * in RevisionStore instead.
- *
- * @param Title $title The title of the page this Revision is associated with.
- * @param UserIdentity $user
- * @param CommentStoreComment $comment
- * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
- * a query that yields the required fields.
- * @param RevisionSlots $slots The slots of this revision.
- * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
- * or false for the local site.
- */
- function __construct(
- Title $title,
- UserIdentity $user,
- CommentStoreComment $comment,
- $row,
- RevisionSlots $slots,
- $wikiId = false
- ) {
- parent::__construct( $title, $slots, $wikiId );
- Assert::parameterType( 'object', $row, '$row' );
-
- $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
- Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
-
- $this->mArchiveId = intval( $row->ar_id );
-
- // NOTE: ar_page_id may be different from $this->mTitle->getArticleID() in some cases,
- // notably when a partially restored page has been moved, and a new page has been created
- // with the same title. Archive rows for that title will then have the wrong page id.
- $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $title->getArticleID();
-
- // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null
- // indicates that the parent revision is unknown. As per MW 1.31, the database schema
- // allows ar_parent_id to be NULL.
- $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null;
- $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null;
- $this->mComment = $comment;
- $this->mUser = $user;
- $this->mTimestamp = $timestamp;
- $this->mMinorEdit = boolval( $row->ar_minor_edit );
- $this->mDeleted = intval( $row->ar_deleted );
- $this->mSize = isset( $row->ar_len ) ? intval( $row->ar_len ) : null;
- $this->mSha1 = !empty( $row->ar_sha1 ) ? $row->ar_sha1 : null;
- }
-
- /**
- * Get archive row ID
- *
- * @return int
- */
- public function getArchiveId() {
- return $this->mArchiveId;
- }
-
- /**
- * @return int|null The revision id, or null if the original revision ID
- * was not recorded in the archive table.
- */
- public function getId() {
- // overwritten just to refine the contract specification.
- return parent::getId();
- }
-
- /**
- * @throws RevisionAccessException if the size was unknown and could not be calculated.
- * @return int The nominal revision size, never null. May be computed on the fly.
- */
- public function getSize() {
- // If length is null, calculate and remember it (potentially SLOW!).
- // This is for compatibility with old database rows that don't have the field set.
- if ( $this->mSize === null ) {
- $this->mSize = $this->mSlots->computeSize();
- }
-
- return $this->mSize;
- }
-
- /**
- * @throws RevisionAccessException if the hash was unknown and could not be calculated.
- * @return string The revision hash, never null. May be computed on the fly.
- */
- public function getSha1() {
- // If hash is null, calculate it and remember (potentially SLOW!)
- // This is for compatibility with old database rows that don't have the field set.
- if ( $this->mSha1 === null ) {
- $this->mSha1 = $this->mSlots->computeSha1();
- }
-
- return $this->mSha1;
- }
-
- /**
- * @param int $audience
- * @param User|null $user
- *
- * @return UserIdentity The identity of the revision author, null if access is forbidden.
- */
- public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- // overwritten just to add a guarantee to the contract
- return parent::getUser( $audience, $user );
- }
-
- /**
- * @param int $audience
- * @param User|null $user
- *
- * @return CommentStoreComment The revision comment, null if access is forbidden.
- */
- public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- // overwritten just to add a guarantee to the contract
- return parent::getComment( $audience, $user );
- }
-
- /**
- * @return string never null
- */
- public function getTimestamp() {
- // overwritten just to add a guarantee to the contract
- return parent::getTimestamp();
- }
-
- /**
- * @see RevisionStore::isComplete
- *
- * @return bool always true.
- */
- public function isReadyForInsertion() {
- return true;
- }
-
-}
+++ /dev/null
-<?php
-/**
- * Service for constructing revision objects.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use MWException;
-use Title;
-
-/**
- * Service for constructing revision objects.
- *
- * @since 1.31
- *
- * @note This was written to act as a drop-in replacement for the corresponding
- * static methods in Revision.
- */
-interface RevisionFactory {
-
- /**
- * Constructs a new RevisionRecord based on the given associative array following the MW1.29
- * database convention for the Revision constructor.
- *
- * MCR migration note: this replaces Revision::newFromRow
- *
- * @deprecated since 1.31. Use a MutableRevisionRecord instead.
- *
- * @param array $fields
- * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
- * @param Title|null $title
- *
- * @return MutableRevisionRecord
- * @throws MWException
- */
- public function newMutableRevisionFromArray( array $fields, $queryFlags = 0, Title $title = null );
-
- /**
- * Constructs a RevisionRecord given a database row and content slots.
- *
- * MCR migration note: this replaces Revision::newFromRow for rows based on the
- * revision, slot, and content tables defined for MCR since MW1.31.
- *
- * @param object $row A query result row as a raw object.
- * Use RevisionStore::getQueryInfo() to build a query that yields the required fields.
- * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
- * @param Title|null $title
- *
- * @return RevisionRecord
- */
- public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null );
-
- /**
- * 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 A query result row as a raw object.
- * Use RevisionStore::getArchiveQueryInfo() to build a query that yields the
- * required fields.
- * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
- * @param Title|null $title
- * @param array $overrides An associative array that allows fields in $row to be overwritten.
- * Keys in this array correspond to field names in $row without the "ar_" prefix, so
- * $overrides['user'] will override $row->ar_user, etc.
- *
- * @return RevisionRecord
- */
- public function newRevisionFromArchiveRow(
- $row,
- $queryFlags = 0,
- Title $title = null,
- array $overrides = []
- );
-
-}
+++ /dev/null
-<?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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use IDBAccessObject;
-use MediaWiki\Linker\LinkTarget;
-use Title;
-
-/**
- * Service for looking up page revisions.
- *
- * @note This was written to act as a drop-in replacement for the corresponding
- * static methods in Revision.
- *
- * @since 1.31
- */
-interface RevisionLookup extends IDBAccessObject {
-
- /**
- * 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:
- *
- * @param int $id
- * @param int $flags bit field, see IDBAccessObject::READ_XXX
- * @return RevisionRecord|null
- */
- public function getRevisionById( $id, $flags = 0 );
-
- /**
- * 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
- *
- * @param LinkTarget $linkTarget
- * @param int $revId (optional)
- * @param int $flags bit field, see IDBAccessObject::READ_XXX
- * @return RevisionRecord|null
- */
- public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 );
-
- /**
- * 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
- *
- * @param int $pageId
- * @param int $revId (optional)
- * @param int $flags bit field, see IDBAccessObject::READ_XXX
- * @return RevisionRecord|null
- */
- public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 );
-
- /**
- * Get previous revision for this title
- *
- * MCR migration note: this replaces Revision::getPrevious
- *
- * @param RevisionRecord $rev
- * @param Title|null $title if known (optional)
- *
- * @return RevisionRecord|null
- */
- public function getPreviousRevision( RevisionRecord $rev, Title $title = null );
-
- /**
- * Get next revision for this title
- *
- * MCR migration note: this replaces Revision::getNext
- *
- * @param RevisionRecord $rev
- * @param Title|null $title if known (optional)
- *
- * @return RevisionRecord|null
- */
- public function getNextRevision( RevisionRecord $rev, Title $title = null );
-
- /**
- * 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
- *
- * @return RevisionRecord|bool Returns false if missing
- */
- public function getKnownCurrentRevision( Title $title, $revId );
-
-}
+++ /dev/null
-<?php
-/**
- * Page revision base class.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use Content;
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\User\UserIdentity;
-use MWException;
-use Title;
-use User;
-use Wikimedia\Assert\Assert;
-
-/**
- * Page revision base class.
- *
- * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
- * Note that while the base class has no setters, subclasses may offer a mutable interface.
- *
- * @since 1.31
- */
-abstract class RevisionRecord {
-
- // RevisionRecord deletion constants
- const DELETED_TEXT = 1;
- const DELETED_COMMENT = 2;
- const DELETED_USER = 4;
- const DELETED_RESTRICTED = 8;
- const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED; // convenience
- const SUPPRESSED_ALL = self::DELETED_TEXT | self::DELETED_COMMENT | self::DELETED_USER |
- self::DELETED_RESTRICTED; // convenience
-
- // Audience options for accessors
- const FOR_PUBLIC = 1;
- const FOR_THIS_USER = 2;
- const RAW = 3;
-
- /** @var string Wiki ID; false means the current wiki */
- protected $mWiki = false;
- /** @var int|null */
- protected $mId;
- /** @var int|null */
- protected $mPageId;
- /** @var UserIdentity|null */
- protected $mUser;
- /** @var bool */
- protected $mMinorEdit = false;
- /** @var string|null */
- protected $mTimestamp;
- /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
- protected $mDeleted = 0;
- /** @var int|null */
- protected $mSize;
- /** @var string|null */
- protected $mSha1;
- /** @var int|null */
- protected $mParentId;
- /** @var CommentStoreComment|null */
- protected $mComment;
-
- /** @var Title */
- protected $mTitle; // TODO: we only need the title for permission checks!
-
- /** @var RevisionSlots */
- protected $mSlots;
-
- /**
- * @note Avoid calling this constructor directly. Use the appropriate methods
- * in RevisionStore instead.
- *
- * @param Title $title The title of the page this Revision is associated with.
- * @param RevisionSlots $slots The slots of this revision.
- * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
- * or false for the local site.
- *
- * @throws MWException
- */
- function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
- Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
-
- $this->mTitle = $title;
- $this->mSlots = $slots;
- $this->mWiki = $wikiId;
-
- // XXX: this is a sensible default, but we may not have a Title object here in the future.
- $this->mPageId = $title->getArticleID();
- }
-
- /**
- * Implemented to defy serialization.
- *
- * @throws LogicException always
- */
- public function __sleep() {
- throw new LogicException( __CLASS__ . ' is not serializable.' );
- }
-
- /**
- * @param RevisionRecord $rec
- *
- * @return bool True if this RevisionRecord is known to have same content as $rec.
- * False if the content is different (or not known to be the same).
- */
- public function hasSameContent( RevisionRecord $rec ) {
- if ( $rec === $this ) {
- return true;
- }
-
- if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
- return true;
- }
-
- // check size before hash, since size is quicker to compute
- if ( $this->getSize() !== $rec->getSize() ) {
- return false;
- }
-
- // instead of checking the hash, we could also check the content addresses of all slots.
-
- if ( $this->getSha1() === $rec->getSha1() ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Returns the Content of the given slot of this revision.
- * Call getSlotNames() to get a list of available slots.
- *
- * Note that for mutable Content objects, each call to this method will return a
- * fresh clone.
- *
- * MCR migration note: this replaces Revision::getContent
- *
- * @param string $role The role name of the desired slot
- * @param int $audience
- * @param User|null $user
- *
- * @throws RevisionAccessException if the slot does not exist or slot data
- * could not be lazy-loaded.
- * @return Content|null The content of the given slot, or null if access is forbidden.
- */
- public function getContent( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
- // XXX: throwing an exception would be nicer, but would a further
- // departure from the signature of Revision::getContent(), and thus
- // more complex and error prone refactoring.
- if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
- return null;
- }
-
- $content = $this->getSlot( $role, $audience, $user )->getContent();
- return $content->copy();
- }
-
- /**
- * Returns meta-data for the given slot.
- *
- * @param string $role The role name of the desired slot
- * @param int $audience
- * @param User|null $user
- *
- * @throws RevisionAccessException if the slot does not exist or slot data
- * could not be lazy-loaded.
- * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
- * calling getContent() on the SlotRecord will throw an exception.
- */
- public function getSlot( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
- $slot = $this->mSlots->getSlot( $role );
-
- if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
- return SlotRecord::newWithSuppressedContent( $slot );
- }
-
- return $slot;
- }
-
- /**
- * Returns whether the given slot is defined in this revision.
- *
- * @param string $role The role name of the desired slot
- *
- * @return bool
- */
- public function hasSlot( $role ) {
- return $this->mSlots->hasSlot( $role );
- }
-
- /**
- * Returns the slot names (roles) of all slots present in this revision.
- * getContent() will succeed only for the names returned by this method.
- *
- * @return string[]
- */
- public function getSlotRoles() {
- return $this->mSlots->getSlotRoles();
- }
-
- /**
- * Returns the slots defined for this revision.
- *
- * @return RevisionSlots
- */
- public function getSlots() {
- return $this->mSlots;
- }
-
- /**
- * Returns the slots that originate in this revision.
- *
- * Note that this does not include any slots inherited from some earlier revision,
- * even if they are different from the slots in the immediate parent revision.
- * This is the case for rollbacks: slots of a rollback revision are inherited from
- * the rollback target, and are different from the slots in the parent revision,
- * which was rolled back.
- *
- * To find all slots modified by this revision against its immediate parent
- * revision, use RevisionSlotsUpdate::newFromRevisionSlots().
- *
- * @return RevisionSlots
- */
- public function getOriginalSlots() {
- return new RevisionSlots( $this->mSlots->getOriginalSlots() );
- }
-
- /**
- * Returns slots inherited from some previous revision.
- *
- * "Inherited" slots are all slots that do not originate in this revision.
- * Note that these slots may still differ from the one in the parent revision.
- * This is the case for rollbacks: slots of a rollback revision are inherited from
- * the rollback target, and are different from the slots in the parent revision,
- * which was rolled back.
- *
- * @return RevisionSlots
- */
- public function getInheritedSlots() {
- return new RevisionSlots( $this->mSlots->getInheritedSlots() );
- }
-
- /**
- * Get revision ID. Depending on the concrete subclass, this may return null if
- * the revision ID is not known (e.g. because the revision does not yet exist
- * in the database).
- *
- * MCR migration note: this replaces Revision::getId
- *
- * @return int|null
- */
- public function getId() {
- return $this->mId;
- }
-
- /**
- * Get parent revision ID (the original previous page revision).
- * If there is no parent revision, this returns 0.
- * If the parent revision is undefined or unknown, this returns null.
- *
- * @note As of MW 1.31, the database schema allows the parent ID to be
- * NULL to indicate that it is unknown.
- *
- * MCR migration note: this replaces Revision::getParentId
- *
- * @return int|null
- */
- public function getParentId() {
- return $this->mParentId;
- }
-
- /**
- * Returns the nominal size of this revision, in bogo-bytes.
- * May be calculated on the fly if not known, which may in the worst
- * case may involve loading all content.
- *
- * MCR migration note: this replaces Revision::getSize
- *
- * @throws RevisionAccessException if the size was unknown and could not be calculated.
- * @return int
- */
- abstract public function getSize();
-
- /**
- * Returns the base36 sha1 of this revision. This hash is derived from the
- * hashes of all slots associated with the revision.
- * May be calculated on the fly if not known, which may in the worst
- * case may involve loading all content.
- *
- * MCR migration note: this replaces Revision::getSha1
- *
- * @throws RevisionAccessException if the hash was unknown and could not be calculated.
- * @return string
- */
- abstract public function getSha1();
-
- /**
- * Get the page ID. If the page does not yet exist, the page ID is 0.
- *
- * MCR migration note: this replaces Revision::getPage
- *
- * @return int
- */
- public function getPageId() {
- return $this->mPageId;
- }
-
- /**
- * Get the ID of the wiki this revision belongs to.
- *
- * @return string|false The wiki's logical name, of false to indicate the local wiki.
- */
- public function getWikiId() {
- return $this->mWiki;
- }
-
- /**
- * Returns the title of the page this revision is associated with as a LinkTarget object.
- *
- * MCR migration note: this replaces Revision::getTitle
- *
- * @return LinkTarget
- */
- public function getPageAsLinkTarget() {
- return $this->mTitle;
- }
-
- /**
- * Fetch revision's author's user identity, if it's available to the specified audience.
- * If the specified audience does not have access to it, null will be
- * returned. Depending on the concrete subclass, null may also be returned if the user is
- * not yet specified.
- *
- * MCR migration note: this replaces Revision::getUser
- *
- * @param int $audience One of:
- * RevisionRecord::FOR_PUBLIC to be displayed to all users
- * RevisionRecord::FOR_THIS_USER to be displayed to the given user
- * RevisionRecord::RAW get the ID regardless of permissions
- * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
- * to the $audience parameter
- * @return UserIdentity|null
- */
- public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
- return null;
- } else {
- return $this->mUser;
- }
- }
-
- /**
- * Fetch revision comment, if it's available to the specified audience.
- * If the specified audience does not have access to the comment,
- * this will return null. Depending on the concrete subclass, null may also be returned
- * if the comment is not yet specified.
- *
- * MCR migration note: this replaces Revision::getComment
- *
- * @param int $audience One of:
- * RevisionRecord::FOR_PUBLIC to be displayed to all users
- * RevisionRecord::FOR_THIS_USER to be displayed to the given user
- * RevisionRecord::RAW get the text regardless of permissions
- * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
- * to the $audience parameter
- *
- * @return CommentStoreComment|null
- */
- public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
- return null;
- } else {
- return $this->mComment;
- }
- }
-
- /**
- * MCR migration note: this replaces Revision::isMinor
- *
- * @return bool
- */
- public function isMinor() {
- return (bool)$this->mMinorEdit;
- }
-
- /**
- * MCR migration note: this replaces Revision::isDeleted
- *
- * @param int $field One of DELETED_* bitfield constants
- *
- * @return bool
- */
- public function isDeleted( $field ) {
- return ( $this->getVisibility() & $field ) == $field;
- }
-
- /**
- * Get the deletion bitfield of the revision
- *
- * MCR migration note: this replaces Revision::getVisibility
- *
- * @return int
- */
- public function getVisibility() {
- return (int)$this->mDeleted;
- }
-
- /**
- * MCR migration note: this replaces Revision::getTimestamp.
- *
- * May return null if the timestamp was not specified.
- *
- * @return string|null
- */
- public function getTimestamp() {
- return $this->mTimestamp;
- }
-
- /**
- * Check that the given audience has access to the given field.
- *
- * MCR migration note: this corresponds to Revision::userCan
- *
- * @param int $field One of self::DELETED_TEXT,
- * self::DELETED_COMMENT,
- * self::DELETED_USER
- * @param int $audience One of:
- * RevisionRecord::FOR_PUBLIC to be displayed to all users
- * RevisionRecord::FOR_THIS_USER to be displayed to the given user
- * RevisionRecord::RAW get the text regardless of permissions
- * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
- * ignored otherwise.
- *
- * @return bool
- */
- public function audienceCan( $field, $audience, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
- return false;
- } elseif ( $audience == self::FOR_THIS_USER ) {
- if ( !$user ) {
- throw new InvalidArgumentException(
- 'A User object must be given when checking FOR_THIS_USER audience.'
- );
- }
-
- if ( !$this->userCan( $field, $user ) ) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Determine if the current user is allowed to view a particular
- * field of this revision, if it's marked as deleted.
- *
- * MCR migration note: this corresponds to Revision::userCan
- *
- * @param int $field One of self::DELETED_TEXT,
- * self::DELETED_COMMENT,
- * self::DELETED_USER
- * @param User $user User object to check
- * @return bool
- */
- protected function userCan( $field, User $user ) {
- // TODO: use callback for permission checks, so we don't need to know a Title object!
- return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
- }
-
- /**
- * Determine if the current user is allowed to view a particular
- * field of this revision, if it's marked as deleted. This is used
- * by various classes to avoid duplication.
- *
- * MCR migration note: this replaces Revision::userCanBitfield
- *
- * @param int $bitfield Current field
- * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
- * self::DELETED_COMMENT = File::DELETED_COMMENT,
- * self::DELETED_USER = File::DELETED_USER
- * @param User $user User object to check
- * @param Title|null $title A Title object to check for per-page restrictions on,
- * instead of just plain userrights
- * @return bool
- */
- public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
- if ( $bitfield & $field ) { // aspect is deleted
- if ( $bitfield & self::DELETED_RESTRICTED ) {
- $permissions = [ 'suppressrevision', 'viewsuppressed' ];
- } elseif ( $field & self::DELETED_TEXT ) {
- $permissions = [ 'deletedtext' ];
- } else {
- $permissions = [ 'deletedhistory' ];
- }
- $permissionlist = implode( ', ', $permissions );
- if ( $title === null ) {
- wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
- return $user->isAllowedAny( ...$permissions );
- } else {
- $text = $title->getPrefixedText();
- wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
- foreach ( $permissions as $perm ) {
- if ( $title->userCan( $perm, $user ) ) {
- return true;
- }
- }
- return false;
- }
- } else {
- return true;
- }
- }
-
- /**
- * Returns whether this RevisionRecord is ready for insertion, that is, whether it contains all
- * information needed to save it to the database. This should trivially be true for
- * RevisionRecords loaded from the database.
- *
- * Note that this may return true even if getId() or getPage() return null or 0, since these
- * are generally assigned while the revision is saved to the database, and may not be available
- * before.
- *
- * @return bool
- */
- public function isReadyForInsertion() {
- // NOTE: don't check getSize() and getSha1(), since that may cause the full content to
- // be loaded in order to calculate the values. Just assume these methods will not return
- // null if mSlots is not empty.
-
- // NOTE: getId() and getPageId() may return null before a revision is saved, so don't
- //check them.
-
- return $this->getTimestamp() !== null
- && $this->getComment( self::RAW ) !== null
- && $this->getUser( self::RAW ) !== null
- && $this->mSlots->getSlotRoles() !== [];
- }
-
-}
+++ /dev/null
-<?php
-/**
- * Value object representing the set of slots belonging to a revision.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use Content;
-use LogicException;
-use Wikimedia\Assert\Assert;
-
-/**
- * Value object representing the set of slots belonging to a revision.
- *
- * @since 1.31
- */
-class RevisionSlots {
-
- /** @var SlotRecord[]|callable */
- protected $slots;
-
- /**
- * @param SlotRecord[]|callable $slots SlotRecords,
- * or a callback that returns such a structure.
- */
- public function __construct( $slots ) {
- Assert::parameterType( 'array|callable', $slots, '$slots' );
-
- if ( is_callable( $slots ) ) {
- $this->slots = $slots;
- } else {
- $this->setSlotsInternal( $slots );
- }
- }
-
- /**
- * @param SlotRecord[] $slots
- */
- private function setSlotsInternal( array $slots ) {
- Assert::parameterElementType( SlotRecord::class, $slots, '$slots' );
-
- $this->slots = [];
-
- // re-key the slot array
- foreach ( $slots as $slot ) {
- $role = $slot->getRole();
- $this->slots[$role] = $slot;
- }
- }
-
- /**
- * Implemented to defy serialization.
- *
- * @throws LogicException always
- */
- public function __sleep() {
- throw new LogicException( __CLASS__ . ' is not serializable.' );
- }
-
- /**
- * Returns the Content of the given slot.
- * Call getSlotNames() to get a list of available slots.
- *
- * Note that for mutable Content objects, each call to this method will return a
- * fresh clone.
- *
- * @param string $role The role name of the desired slot
- *
- * @throws RevisionAccessException if the slot does not exist or slot data
- * could not be lazy-loaded.
- * @return Content
- */
- public function getContent( $role ) {
- // Return a copy to be safe. Immutable content objects return $this from copy().
- return $this->getSlot( $role )->getContent()->copy();
- }
-
- /**
- * Returns the SlotRecord of the given slot.
- * Call getSlotNames() to get a list of available slots.
- *
- * @param string $role The role name of the desired slot
- *
- * @throws RevisionAccessException if the slot does not exist or slot data
- * could not be lazy-loaded.
- * @return SlotRecord
- */
- public function getSlot( $role ) {
- $slots = $this->getSlots();
-
- if ( isset( $slots[$role] ) ) {
- return $slots[$role];
- } else {
- throw new RevisionAccessException( 'No such slot: ' . $role );
- }
- }
-
- /**
- * Returns whether the given slot is set.
- *
- * @param string $role The role name of the desired slot
- *
- * @return bool
- */
- public function hasSlot( $role ) {
- $slots = $this->getSlots();
-
- return isset( $slots[$role] );
- }
-
- /**
- * Returns the slot names (roles) of all slots present in this revision.
- * getContent() will succeed only for the names returned by this method.
- *
- * @return string[]
- */
- public function getSlotRoles() {
- $slots = $this->getSlots();
- return array_keys( $slots );
- }
-
- /**
- * Computes the total nominal size of the revision's slots, in bogo-bytes.
- *
- * @warning This is potentially expensive! It may cause all slot's content to be loaded
- * and deserialized.
- *
- * @return int
- */
- public function computeSize() {
- return array_reduce( $this->getSlots(), function ( $accu, SlotRecord $slot ) {
- return $accu + $slot->getSize();
- }, 0 );
- }
-
- /**
- * Returns an associative array that maps role names to SlotRecords. Each SlotRecord
- * represents the content meta-data of a slot, together they define the content of
- * a revision.
- *
- * @note This may cause the content meta-data for the revision to be lazy-loaded.
- *
- * @return SlotRecord[] revision slot/content rows, keyed by slot role name.
- */
- public function getSlots() {
- if ( is_callable( $this->slots ) ) {
- $slots = call_user_func( $this->slots );
-
- Assert::postcondition(
- is_array( $slots ),
- 'Slots info callback should return an array of objects'
- );
-
- $this->setSlotsInternal( $slots );
- }
-
- return $this->slots;
- }
-
- /**
- * Computes the combined hash of the revisions's slots.
- *
- * @note For backwards compatibility, the combined hash of a single slot
- * is that slot's hash. For consistency, the combined hash of an empty set of slots
- * is the hash of the empty string.
- *
- * @warning This is potentially expensive! It may cause all slot's content to be loaded
- * and deserialized, then re-serialized and hashed.
- *
- * @return string
- */
- public function computeSha1() {
- $slots = $this->getSlots();
- ksort( $slots );
-
- if ( empty( $slots ) ) {
- return SlotRecord::base36Sha1( '' );
- }
-
- return array_reduce( $slots, function ( $accu, SlotRecord $slot ) {
- return $accu === null
- ? $slot->getSha1()
- : SlotRecord::base36Sha1( $accu . $slot->getSha1() );
- }, null );
- }
-
- /**
- * Return all slots that belong to the revision they originate from (that is,
- * they are not inherited from some other revision).
- *
- * @note This may cause the slot meta-data for the revision to be lazy-loaded.
- *
- * @return SlotRecord[]
- */
- public function getOriginalSlots() {
- return array_filter(
- $this->getSlots(),
- function ( SlotRecord $slot ) {
- return !$slot->isInherited();
- }
- );
- }
-
- /**
- * Return all slots that are not not originate in the revision they belong to (that is,
- * they are inherited from some other revision).
- *
- * @note This may cause the slot meta-data for the revision to be lazy-loaded.
- *
- * @return SlotRecord[]
- */
- public function getInheritedSlots() {
- return array_filter(
- $this->getSlots(),
- function ( SlotRecord $slot ) {
- return $slot->isInherited();
- }
- );
- }
-
- /**
- * Checks whether the other RevisionSlots instance has the same content
- * as this instance. Note that this does not mean that the slots have to be the same:
- * they could for instance belong to different revisions.
- *
- * @param RevisionSlots $other
- *
- * @return bool
- */
- public function hasSameContent( RevisionSlots $other ) {
- if ( $other === $this ) {
- return true;
- }
-
- $aSlots = $this->getSlots();
- $bSlots = $other->getSlots();
-
- ksort( $aSlots );
- ksort( $bSlots );
-
- if ( array_keys( $aSlots ) !== array_keys( $bSlots ) ) {
- return false;
- }
-
- foreach ( $aSlots as $role => $s ) {
- $t = $bSlots[$role];
-
- if ( !$s->hasSameContent( $t ) ) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Find roles for which the $other RevisionSlots object has different content
- * as this RevisionSlots object, including any roles that are present in one
- * but not the other.
- *
- * @param RevisionSlots $other
- *
- * @return string[] a list of slot roles that are different.
- */
- public function getRolesWithDifferentContent( RevisionSlots $other ) {
- if ( $other === $this ) {
- return [];
- }
-
- $aSlots = $this->getSlots();
- $bSlots = $other->getSlots();
-
- ksort( $aSlots );
- ksort( $bSlots );
-
- $different = array_keys( array_merge(
- array_diff_key( $aSlots, $bSlots ),
- array_diff_key( $bSlots, $aSlots )
- ) );
-
- /** @var SlotRecord[] $common */
- $common = array_intersect_key( $aSlots, $bSlots );
-
- foreach ( $common as $role => $s ) {
- $t = $bSlots[$role];
-
- if ( !$s->hasSameContent( $t ) ) {
- $different[] = $role;
- }
- }
-
- return $different;
- }
-
-}
namespace MediaWiki\Storage;
use Content;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\SlotRecord;
/**
* Value object representing a modification of revision slots.
+++ /dev/null
-<?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
- */
-
-namespace MediaWiki\Storage;
-
-use ActorMigration;
-use CommentStore;
-use CommentStoreComment;
-use Content;
-use ContentHandler;
-use DBAccessObjectUtils;
-use Hooks;
-use IDBAccessObject;
-use InvalidArgumentException;
-use IP;
-use LogicException;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserIdentityValue;
-use Message;
-use MWException;
-use MWUnknownContentModelException;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use RecentChange;
-use Revision;
-use RuntimeException;
-use stdClass;
-use Title;
-use User;
-use WANObjectCache;
-use Wikimedia\Assert\Assert;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\DBConnRef;
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\ILoadBalancer;
-
-/**
- * Service for looking up page revisions.
- *
- * @since 1.31
- *
- * @note This was written to act as a drop-in replacement for the corresponding
- * static methods in Revision.
- */
-class RevisionStore
- implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
-
- const ROW_CACHE_KEY = 'revision-row-1.29';
-
- /**
- * @var SqlBlobStore
- */
- private $blobStore;
-
- /**
- * @var bool|string
- */
- private $wikiId;
-
- /**
- * @var boolean
- * @see $wgContentHandlerUseDB
- */
- private $contentHandlerUseDB = true;
-
- /**
- * @var ILoadBalancer
- */
- private $loadBalancer;
-
- /**
- * @var WANObjectCache
- */
- private $cache;
-
- /**
- * @var CommentStore
- */
- private $commentStore;
-
- /**
- * @var ActorMigration
- */
- private $actorMigration;
-
- /**
- * @var LoggerInterface
- */
- private $logger;
-
- /**
- * @var NameTableStore
- */
- private $contentModelStore;
-
- /**
- * @var NameTableStore
- */
- private $slotRoleStore;
-
- /** @var int An appropriate combination of SCHEMA_COMPAT_XXX flags. */
- private $mcrMigrationStage;
-
- /**
- * @todo $blobStore should be allowed to be any BlobStore!
- *
- * @param ILoadBalancer $loadBalancer
- * @param SqlBlobStore $blobStore
- * @param WANObjectCache $cache A cache for caching revision rows. This can be the local
- * wiki's default instance even if $wikiId refers to a different wiki, since
- * 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.
- * @param CommentStore $commentStore
- * @param NameTableStore $contentModelStore
- * @param NameTableStore $slotRoleStore
- * @param int $mcrMigrationStage An appropriate combination of SCHEMA_COMPAT_XXX flags
- * @param ActorMigration $actorMigration
- * @param bool|string $wikiId
- *
- * @throws MWException if $mcrMigrationStage or $wikiId is invalid.
- */
- public function __construct(
- ILoadBalancer $loadBalancer,
- SqlBlobStore $blobStore,
- WANObjectCache $cache,
- CommentStore $commentStore,
- NameTableStore $contentModelStore,
- NameTableStore $slotRoleStore,
- $mcrMigrationStage,
- ActorMigration $actorMigration,
- $wikiId = false
- ) {
- Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
- Assert::parameterType( 'integer', $mcrMigrationStage, '$mcrMigrationStage' );
- Assert::parameter(
- ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== SCHEMA_COMPAT_READ_BOTH,
- '$mcrMigrationStage',
- 'Reading from the old and the new schema at the same time is not supported.'
- );
- Assert::parameter(
- ( $mcrMigrationStage & SCHEMA_COMPAT_READ_BOTH ) !== 0,
- '$mcrMigrationStage',
- 'Reading needs to be enabled for the old or the new schema.'
- );
- Assert::parameter(
- ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_BOTH ) !== 0,
- '$mcrMigrationStage',
- 'Writing needs to be enabled for the old or the new schema.'
- );
- Assert::parameter(
- ( $mcrMigrationStage & SCHEMA_COMPAT_READ_OLD ) === 0
- || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) !== 0,
- '$mcrMigrationStage',
- 'Cannot read the old schema when not also writing it.'
- );
- Assert::parameter(
- ( $mcrMigrationStage & SCHEMA_COMPAT_READ_NEW ) === 0
- || ( $mcrMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) !== 0,
- '$mcrMigrationStage',
- 'Cannot read the new schema when not also writing it.'
- );
-
- $this->loadBalancer = $loadBalancer;
- $this->blobStore = $blobStore;
- $this->cache = $cache;
- $this->commentStore = $commentStore;
- $this->contentModelStore = $contentModelStore;
- $this->slotRoleStore = $slotRoleStore;
- $this->mcrMigrationStage = $mcrMigrationStage;
- $this->actorMigration = $actorMigration;
- $this->wikiId = $wikiId;
- $this->logger = new NullLogger();
- }
-
- /**
- * @param int $flags A combination of SCHEMA_COMPAT_XXX flags.
- * @return bool True if all the given flags were set in the $mcrMigrationStage
- * parameter passed to the constructor.
- */
- private function hasMcrSchemaFlags( $flags ) {
- return ( $this->mcrMigrationStage & $flags ) === $flags;
- }
-
- /**
- * Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading
- * and still reading from the old DB schema.
- *
- * @throws RevisionAccessException
- */
- private function assertCrossWikiContentLoadingIsSafe() {
- if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
- throw new RevisionAccessException(
- "Cross-wiki content loading is not supported by the pre-MCR schema"
- );
- }
- }
-
- public function setLogger( LoggerInterface $logger ) {
- $this->logger = $logger;
- }
-
- /**
- * @return bool Whether the store is read-only
- */
- public function isReadOnly() {
- return $this->blobStore->isReadOnly();
- }
-
- /**
- * @return bool
- */
- public function getContentHandlerUseDB() {
- return $this->contentHandlerUseDB;
- }
-
- /**
- * @see $wgContentHandlerUseDB
- * @param bool $contentHandlerUseDB
- * @throws MWException
- */
- public function setContentHandlerUseDB( $contentHandlerUseDB ) {
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
- || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
- ) {
- if ( !$contentHandlerUseDB ) {
- throw new MWException(
- 'Content model must be stored in the database for multi content revision migration.'
- );
- }
- }
- $this->contentHandlerUseDB = $contentHandlerUseDB;
- }
-
- /**
- * @return ILoadBalancer
- */
- private function getDBLoadBalancer() {
- return $this->loadBalancer;
- }
-
- /**
- * @param int $mode DB_MASTER or DB_REPLICA
- *
- * @return IDatabase
- */
- private function getDBConnection( $mode ) {
- $lb = $this->getDBLoadBalancer();
- return $lb->getConnection( $mode, [], $this->wikiId );
- }
-
- /**
- * @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 );
- }
-
- /**
- * @param IDatabase $connection
- */
- private function releaseDBConnection( IDatabase $connection ) {
- $lb = $this->getDBLoadBalancer();
- $lb->reuseConnection( $connection );
- }
-
- /**
- * @param int $mode DB_MASTER or DB_REPLICA
- *
- * @return DBConnRef
- */
- private function getDBConnectionRef( $mode ) {
- $lb = $this->getDBLoadBalancer();
- return $lb->getConnectionRef( $mode, [], $this->wikiId );
- }
-
- /**
- * Determines the page Title based on the available information.
- *
- * MCR migration note: this corresponds to Revision::getTitle
- *
- * @note this method should be private, external use should be avoided!
- *
- * @param int|null $pageId
- * @param int|null $revId
- * @param int $queryFlags
- *
- * @return Title
- * @throws RevisionAccessException
- */
- public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
- if ( !$pageId && !$revId ) {
- throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
- }
-
- // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
- // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
- if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
- $queryFlags = self::READ_NORMAL;
- }
-
- $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
- list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
- $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
-
- // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
- if ( $canUseTitleNewFromId ) {
- // TODO: better foreign title handling (introduce TitleFactory)
- $title = Title::newFromID( $pageId, $titleFlags );
- if ( $title ) {
- return $title;
- }
- }
-
- // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
- $canUseRevId = ( $revId !== null && $revId > 0 );
-
- if ( $canUseRevId ) {
- $dbr = $this->getDBConnectionRef( $dbMode );
- // @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)
- return Title::newFromRow( $row );
- }
- }
-
- // If we still don't have a title, fallback to master if that wasn't already happening.
- if ( $dbMode !== DB_MASTER ) {
- $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
- if ( $title ) {
- $this->logger->info(
- __METHOD__ . ' fell back to READ_LATEST and got a Title.',
- [ 'trace' => wfBacktrace() ]
- );
- return $title;
- }
- }
-
- throw new RevisionAccessException(
- "Could not determine title for page ID $pageId and revision ID $revId"
- );
- }
-
- /**
- * @param mixed $value
- * @param string $name
- *
- * @throws IncompleteRevisionException if $value is null
- * @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
- *
- * @throws IncompleteRevisionException if $value is empty
- * @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;
- }
-
- /**
- * Insert a new revision into the database, returning the new revision record
- * on success and dies horribly on failure.
- *
- * MCR migration note: this replaces Revision::insertOn
- *
- * @param RevisionRecord $rev
- * @param IDatabase $dbw (master connection)
- *
- * @throws InvalidArgumentException
- * @return RevisionRecord the new revision record.
- */
- public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
- // TODO: pass in a DBTransactionContext instead of a database connection.
- $this->checkDatabaseWikiId( $dbw );
-
- $slotRoles = $rev->getSlotRoles();
-
- // Make sure the main slot is always provided throughout migration
- if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
- throw new InvalidArgumentException(
- 'main slot must be provided'
- );
- }
-
- // If we are not writing into the new schema, we can't support extra slots.
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
- && $slotRoles !== [ SlotRecord::MAIN ]
- ) {
- throw new InvalidArgumentException(
- 'Only the main slot is supported when not writing to the MCR enabled schema!'
- );
- }
-
- // As long as we are not reading from the new schema, we don't want to write extra slots.
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
- && $slotRoles !== [ SlotRecord::MAIN ]
- ) {
- throw new InvalidArgumentException(
- 'Only the main slot is supported when not reading from the MCR enabled schema!'
- );
- }
-
- // 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' );
-
- 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' );
- }
-
- // 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();
-
- /** @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
- );
- }
- );
-
- // 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'
- );
-
- // Trigger exception if the main slot is missing.
- // Technically, this could go away after MCR migration: while
- // calling code may require a main slot to exist, RevisionStore
- // really should not know or care about that requirement.
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
-
- foreach ( $slotRoles as $role ) {
- $slot = $rev->getSlot( $role, RevisionRecord::RAW );
- Assert::postcondition(
- $slot->getContent() !== null,
- $role . ' slot must have content'
- );
- Assert::postcondition(
- $slot->hasRevision(),
- $role . ' slot must have a revision associated'
- );
- }
-
- Hooks::run( 'RevisionRecordInserted', [ $rev ] );
-
- // TODO: deprecate in 1.32!
- $legacyRevision = new Revision( $rev );
- Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
-
- return $rev;
- }
-
- private function insertRevisionInternal(
- RevisionRecord $rev,
- IDatabase $dbw,
- User $user,
- CommentStoreComment $comment,
- Title $title,
- $pageId,
- $parentId
- ) {
- $slotRoles = $rev->getSlotRoles();
-
- $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 );
-
- // 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() ) {
- // 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;
-
- // Write the main slot's text ID to the revision table for backwards compatibility
- if ( $slot->getRole() === SlotRecord::MAIN
- && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
- ) {
- $blobAddress = $slot->getAddress();
- $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
- }
- } else {
- $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
- }
- }
-
- $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
-
- $rev = new RevisionStoreRecord(
- $title,
- $user,
- $comment,
- (object)$revisionRow,
- new RevisionSlots( $newSlots ),
- $this->wikiId
- );
-
- return $rev;
- }
-
- /**
- * @param IDatabase $dbw
- * @param int $revisionId
- * @param string &$blobAddress (may change!)
- *
- * @return int the text row id
- */
- private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
- $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
- if ( !$textId ) {
- throw new LogicException(
- 'Blob address not supported in 1.29 database schema: ' . $blobAddress
- );
- }
-
- // getTextIdFromAddress() is free to insert something into the text table, so $textId
- // may be a new value, not anything already contained in $blobAddress.
- $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
-
- $dbw->update(
- 'revision',
- [ 'rev_text_id' => $textId ],
- [ 'rev_id' => $revisionId ],
- __METHOD__
- );
-
- return $textId;
- }
-
- /**
- * @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();
- } else {
- $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
- }
-
- $contentId = null;
-
- // Write the main slot's text ID to the revision table for backwards compatibility
- if ( $protoSlot->getRole() === SlotRecord::MAIN
- && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
- ) {
- // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
- // with the real content ID below.
- $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
- $contentId = $this->emulateContentId( $textId );
- }
-
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
- if ( $protoSlot->hasContentId() ) {
- $contentId = $protoSlot->getContentId();
- } else {
- $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
- }
-
- $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
- }
-
- $savedSlot = SlotRecord::newSaved(
- $revisionId,
- $contentId,
- $blobAddress,
- $protoSlot
- );
-
- return $savedSlot;
- }
-
- /**
- * 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
- ) {
- if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
- $ipcRow = [
- 'ipc_rev_id' => $revisionId,
- 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
- 'ipc_hex' => IP::toHex( $user->getName() ),
- ];
- $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
- }
- }
-
- /**
- * @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 );
-
- list( $commentFields, $commentCallback ) =
- $this->commentStore->insertWithTempTable(
- $dbw,
- 'rev_comment',
- $rev->getComment( RevisionRecord::RAW )
- );
- $revisionRow += $commentFields;
-
- list( $actorFields, $actorCallback ) =
- $this->actorMigration->getInsertValuesWithTempTable(
- $dbw,
- 'rev_user',
- $rev->getUser( RevisionRecord::RAW )
- );
- $revisionRow += $actorFields;
-
- $dbw->insert( 'revision', $revisionRow, __METHOD__ );
-
- if ( !isset( $revisionRow['rev_id'] ) ) {
- // only if auto-increment was used
- $revisionRow['rev_id'] = intval( $dbw->insertId() );
-
- 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';
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
- $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) );
- if ( $maxRevId2 >= $maxRevId ) {
- $maxRevId = $maxRevId2;
- $table = 'slots';
- }
- }
-
- 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__;
- $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) {
- $dbw->unlock( 'fix-for-T202032', $fname );
- } );
-
- $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(
- $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE'
- )->fetchObject();
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
- $row2 = $dbw->query(
- $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ )
- . ' FOR UPDATE'
- )->fetchObject();
- } else {
- $row2 = null;
- }
- $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__ );
- }
- }
- }
-
- $commentCallback( $revisionRow['rev_id'] );
- $actorCallback( $revisionRow['rev_id'], $revisionRow );
-
- return $revisionRow;
- }
-
- /**
- * @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(),
- ];
-
- if ( $rev->getId() !== null ) {
- // Needed to restore revisions with their original ID
- $revisionRow['rev_id'] = $rev->getId();
- }
-
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
- // In non MCR mode this IF section will relate to the main slot
- $mainSlot = $rev->getSlot( SlotRecord::MAIN );
- $model = $mainSlot->getModel();
- $format = $mainSlot->getFormat();
-
- // MCR migration note: rev_content_model and rev_content_format will go away
- if ( $this->contentHandlerUseDB ) {
- $this->assertCrossWikiContentLoadingIsSafe();
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
-
- $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
- $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
- }
- }
-
- return $revisionRow;
- }
-
- /**
- * @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();
-
- $this->checkContent( $content, $title );
-
- 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,
- ]
- )
- );
- }
-
- /**
- * @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() );
- }
-
- /**
- * MCR migration note: this corresponds to Revision::checkContentModel
- *
- * @param Content $content
- * @param Title $title
- *
- * @throws MWException
- * @throws MWUnknownContentModelException
- */
- private function checkContent( Content $content, Title $title ) {
- // 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 ( !$this->contentHandlerUseDB ) {
- // if $wgContentHandlerUseDB is not set,
- // all revisions must use the default content model and format.
-
- $this->assertCrossWikiContentLoadingIsSafe();
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultHandler = ContentHandler::getForModelID( $defaultModel );
- $defaultFormat = $defaultHandler->getDefaultFormat();
-
- if ( $model != $defaultModel ) {
- throw new MWException( "Can't save non-default content model with "
- . "\$wgContentHandlerUseDB disabled: model is $model, "
- . "default for $name is $defaultModel"
- );
- }
-
- if ( $format != $defaultFormat ) {
- throw new MWException( "Can't use non-default content format with "
- . "\$wgContentHandlerUseDB disabled: format is $format, "
- . "default for $name is $defaultFormat"
- );
- }
- }
-
- 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.
- *
- * @note This method grabs a FOR UPDATE lock on the relevant row of the page table,
- * to prevent a new revision from being inserted before the null revision has been written
- * to the database.
- *
- * MCR migration note: this replaces Revision::newNullRevision
- *
- * @todo Introduce newFromParentRevision(). newNullRevision can then be based on that
- * (or go away).
- *
- * @param IDatabase $dbw used for obtaining the lock on the page table row
- * @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
- *
- * @return RevisionRecord|null RevisionRecord or null on error
- */
- public function newNullRevision(
- IDatabase $dbw,
- Title $title,
- CommentStoreComment $comment,
- $minor,
- User $user
- ) {
- $this->checkDatabaseWikiId( $dbw );
-
- $pageId = $title->getArticleID();
-
- // 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',
- [ 'page_id' => $pageId ],
- __METHOD__,
- [ 'FOR UPDATE' ]
- );
-
- if ( !$pageLatest ) {
- return null;
- }
-
- // Fetch the actual revision row from master, without locking all extra tables.
- $oldRevision = $this->loadRevisionFromConds(
- $dbw,
- [ 'rev_id' => intval( $pageLatest ) ],
- self::READ_LATEST,
- $title
- );
-
- if ( !$oldRevision ) {
- $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
- $this->logger->error(
- $msg,
- [ 'exception' => new RuntimeException( $msg ) ]
- );
- return null;
- }
-
- // Construct the new revision
- $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
- $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
-
- $newRevision->setComment( $comment );
- $newRevision->setUser( $user );
- $newRevision->setTimestamp( $timestamp );
- $newRevision->setMinorEdit( $minor );
-
- return $newRevision;
- }
-
- /**
- * MCR migration note: this replaces Revision::isUnpatrolled
- *
- * @todo This is overly specific, so move or kill this method.
- *
- * @param RevisionRecord $rev
- *
- * @return int Rcid of the unpatrolled row, zero if there isn't one
- */
- public function getRcIdIfUnpatrolled( RevisionRecord $rev ) {
- $rc = $this->getRecentChange( $rev );
- if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
- 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 );
- $db = $this->getDBConnection( $dbType );
-
- $userIdentity = $rev->getUser( RevisionRecord::RAW );
-
- if ( !$userIdentity ) {
- // If the revision has no user identity, chances are it never went
- // into the database, and doesn't have an RC entry.
- return null;
- }
-
- // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
- $actorWhere = $this->actorMigration->getWhere( $db, 'rc_user', $rev->getUser(), false );
- $rc = RecentChange::newFromConds(
- [
- $actorWhere['conds'],
- 'rc_timestamp' => $db->timestamp( $rev->getTimestamp() ),
- 'rc_this_oldid' => $rev->getId()
- ],
- __METHOD__,
- $dbType
- );
-
- $this->releaseDBConnection( $db );
-
- // XXX: cache this locally? Glue it to the RevisionRecord?
- return $rc;
- }
-
- /**
- * Maps fields of the archive row to corresponding revision rows.
- *
- * @param object $archiveRow
- *
- * @return object a revision row object, corresponding to $archiveRow.
- */
- private static function mapArchiveFields( $archiveRow ) {
- $fieldMap = [
- // keep with ar prefix:
- 'ar_id' => 'ar_id',
-
- // not the same suffix:
- 'ar_page_id' => 'rev_page',
- 'ar_rev_id' => 'rev_id',
-
- // same suffix:
- 'ar_text_id' => 'rev_text_id',
- 'ar_timestamp' => 'rev_timestamp',
- 'ar_user_text' => 'rev_user_text',
- 'ar_user' => 'rev_user',
- 'ar_actor' => 'rev_actor',
- 'ar_minor_edit' => 'rev_minor_edit',
- 'ar_deleted' => 'rev_deleted',
- 'ar_len' => 'rev_len',
- 'ar_parent_id' => 'rev_parent_id',
- 'ar_sha1' => 'rev_sha1',
- 'ar_comment' => 'rev_comment',
- 'ar_comment_cid' => 'rev_comment_cid',
- 'ar_comment_id' => 'rev_comment_id',
- 'ar_comment_text' => 'rev_comment_text',
- 'ar_comment_data' => 'rev_comment_data',
- 'ar_comment_old' => 'rev_comment_old',
- 'ar_content_format' => 'rev_content_format',
- 'ar_content_model' => 'rev_content_model',
- ];
-
- $revRow = new stdClass();
- foreach ( $fieldMap as $arKey => $revKey ) {
- if ( property_exists( $archiveRow, $arKey ) ) {
- $revRow->$revKey = $archiveRow->$arKey;
- }
- }
-
- return $revRow;
- }
-
- /**
- * Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
- *
- * @param object|array $row Either a database row or an array
- * @param int $queryFlags for callbacks
- * @param Title $title
- *
- * @return SlotRecord The main slot, extracted from the MW 1.29 style row.
- * @throws MWException
- */
- private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
- $mainSlotRow = new stdClass();
- $mainSlotRow->role_name = SlotRecord::MAIN;
- $mainSlotRow->model_name = null;
- $mainSlotRow->slot_revision_id = null;
- $mainSlotRow->slot_content_id = null;
- $mainSlotRow->content_address = null;
-
- $content = null;
- $blobData = null;
- $blobFlags = null;
-
- if ( is_object( $row ) ) {
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
- // Don't emulate from a row when using the new schema.
- // Emulating from an array is still OK.
- throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
- }
-
- // archive row
- if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
- $row = $this->mapArchiveFields( $row );
- }
-
- if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
- $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
- $row->rev_text_id
- );
- }
-
- // This is used by null-revisions
- $mainSlotRow->slot_origin = isset( $row->slot_origin )
- ? intval( $row->slot_origin )
- : null;
-
- if ( isset( $row->old_text ) ) {
- // this happens when the text-table gets joined directly, in the pre-1.30 schema
- $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
- // Check against selects that might have not included old_flags
- if ( !property_exists( $row, 'old_flags' ) ) {
- throw new InvalidArgumentException( 'old_flags was not set in $row' );
- }
- $blobFlags = $row->old_flags ?? '';
- }
-
- $mainSlotRow->slot_revision_id = intval( $row->rev_id );
-
- $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
- $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
- $mainSlotRow->model_name = isset( $row->rev_content_model )
- ? strval( $row->rev_content_model )
- : null;
- // XXX: in the future, we'll probably always use the default format, and drop content_format
- $mainSlotRow->format_name = isset( $row->rev_content_format )
- ? strval( $row->rev_content_format )
- : null;
-
- if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
- // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
- $mainSlotRow->slot_content_id
- = $this->emulateContentId( intval( $row->rev_text_id ) );
- }
- } elseif ( is_array( $row ) ) {
- $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
-
- $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
- ? intval( $row['slot_origin'] )
- : null;
- $mainSlotRow->content_address = isset( $row['text_id'] )
- ? SqlBlobStore::makeAddressFromTextId( intval( $row['text_id'] ) )
- : null;
- $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
- $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
-
- $mainSlotRow->model_name = isset( $row['content_model'] )
- ? strval( $row['content_model'] ) : null; // XXX: must be a string!
- // XXX: in the future, we'll probably always use the default format, and drop content_format
- $mainSlotRow->format_name = isset( $row['content_format'] )
- ? strval( $row['content_format'] ) : null;
- $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
- // XXX: If the flags field is not set then $blobFlags should be null so that no
- // decoding will happen. An empty string will result in default decodings.
- $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
-
- // if we have a Content object, override mText and mContentModel
- if ( !empty( $row['content'] ) ) {
- if ( !( $row['content'] instanceof Content ) ) {
- throw new MWException( 'content field must contain a Content object.' );
- }
-
- /** @var Content $content */
- $content = $row['content'];
- $handler = $content->getContentHandler();
-
- $mainSlotRow->model_name = $content->getModel();
-
- // XXX: in the future, we'll probably always use the default format.
- if ( $mainSlotRow->format_name === null ) {
- $mainSlotRow->format_name = $handler->getDefaultFormat();
- }
- }
-
- if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
- // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
- $mainSlotRow->slot_content_id
- = $this->emulateContentId( intval( $row['text_id'] ) );
- }
- } else {
- throw new MWException( 'Revision constructor passed invalid row format.' );
- }
-
- // With the old schema, the content changes with every revision,
- // except for null-revisions.
- if ( !isset( $mainSlotRow->slot_origin ) ) {
- $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
- }
-
- if ( $mainSlotRow->model_name === null ) {
- $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
- $this->assertCrossWikiContentLoadingIsSafe();
-
- // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
- // TODO: MCR: deprecate $title->getModel().
- return ContentHandler::getDefaultModelFor( $title );
- };
- }
-
- if ( !$content ) {
- // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
- // is missing, but "empty revisions" with no content are used in some edge cases.
-
- $content = function ( SlotRecord $slot )
- use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
- {
- return $this->loadSlotContent(
- $slot,
- $blobData,
- $blobFlags,
- $mainSlotRow->format_name,
- $queryFlags
- );
- };
- }
-
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
- // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
- // the inherited slot to have the same content_id as the original slot. In that case,
- // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
- $mainSlotRow->slot_content_id =
- function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
- $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
- return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
- };
- }
-
- return new SlotRecord( $mainSlotRow, $content );
- }
-
- /**
- * Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode,
- * based on the revision's text ID (rev_text_id or ar_text_id, respectively).
- * Note that in SCHEMA_COMPAT_WRITE_BOTH, a callback to findSlotContentId() should be used
- * instead, since in that mode, some revision rows may already have a real content ID,
- * while other's don't - and for the ones that don't, we should indicate that it
- * is missing and cause SlotRecords::hasContentId() to return false.
- *
- * @param int $textId
- * @return int The emulated content ID
- */
- private function emulateContentId( $textId ) {
- // Return a negative number to ensure the ID is distinct from any real content IDs
- // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
- // mode.
- return -$textId;
- }
-
- /**
- * 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
- * @param string|null $blobFlags Flags indicating how $blobData needs to be processed.
- * Use null if no processing should happen. That is in constrast to the empty string,
- * which causes the blob to be decoded according to the configured legacy encoding.
- * @param string|null $blobFormat MIME type indicating how $dataBlob is encoded
- * @param int $queryFlags
- *
- * @throws RevisionAccessException
- * @return Content
- */
- private function loadSlotContent(
- SlotRecord $slot,
- $blobData = null,
- $blobFlags = null,
- $blobFormat = null,
- $queryFlags = 0
- ) {
- if ( $blobData !== null ) {
- Assert::parameterType( 'string', $blobData, '$blobData' );
- Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
-
- $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
-
- if ( $blobFlags === null ) {
- // No blob flags, so use the blob verbatim.
- $data = $blobData;
- } else {
- $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
- if ( $data === false ) {
- throw new RevisionAccessException(
- "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
- );
- }
- }
-
- } else {
- $address = $slot->getAddress();
- try {
- $data = $this->blobStore->getBlob( $address, $queryFlags );
- } catch ( BlobAccessException $e ) {
- throw new RevisionAccessException(
- "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
- );
- }
- }
-
- // Unserialize content
- $handler = ContentHandler::getForModelID( $slot->getModel() );
-
- $content = $handler->unserializeContent( $data, $blobFormat );
- return $content;
- }
-
- /**
- * 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
- */
- public function getRevisionById( $id, $flags = 0 ) {
- return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
- }
-
- /**
- * 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 ) {
- $conds = [
- 'page_namespace' => $linkTarget->getNamespace(),
- 'page_title' => $linkTarget->getDBkey()
- ];
- 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.
- $db = $this->getDBConnectionRefForQueryFlags( $flags );
-
- $conds[] = 'rev_id=page_latest';
- $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
-
- 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.
- $db = $this->getDBConnectionRefForQueryFlags( $flags );
-
- $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
- *
- * @param Title $title
- * @param string $timestamp
- * @return RevisionRecord|null
- */
- public function getRevisionByTimestamp( $title, $timestamp ) {
- $db = $this->getDBConnection( DB_REPLICA );
- return $this->newRevisionFromConds(
- [
- 'rev_timestamp' => $db->timestamp( $timestamp ),
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ],
- 0,
- $title
- );
- }
-
- /**
- * @param int $revId The revision to load slots for.
- * @param int $queryFlags
- *
- * @return SlotRecord[]
- */
- private function loadSlotRecords( $revId, $queryFlags ) {
- $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']
- );
-
- $slots = [];
-
- foreach ( $res as $row ) {
- // resolve role names and model names from in-memory cache, instead of joining.
- $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
- $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
-
- $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
- return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
- };
-
- $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
- }
-
- if ( !isset( $slots[SlotRecord::MAIN] ) ) {
- throw new RevisionAccessException(
- 'Main slot of revision ' . $revId . ' not found in database!'
- );
- };
-
- return $slots;
- }
-
- /**
- * Factory method for RevisionSlots.
- *
- * @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
- * @param int $queryFlags
- * @param Title $title
- *
- * @return RevisionSlots
- * @throws MWException
- */
- private function newRevisionSlots(
- $revId,
- $revisionRow,
- $queryFlags,
- Title $title
- ) {
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
- $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
- $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
- } else {
- // XXX: do we need the same kind of caching here
- // that getKnownCurrentRevision uses (if $revId == page_latest?)
-
- $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
- return $this->loadSlotRecords( $revId, $queryFlags );
- } );
- }
-
- return $slots;
- }
-
- /**
- * 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 = []
- ) {
- 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;
- }
-
- try {
- $user = User::newFromAnyId(
- $row->ar_user ?? null,
- $row->ar_user_text ?? null,
- $row->ar_actor ?? null
- );
- } catch ( InvalidArgumentException $ex ) {
- wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
- $user = new UserIdentityValue( 0, 'Unknown user', 0 );
- }
-
- $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
- // Legacy because $row may have come from self::selectFields()
- $comment = $this->commentStore->getCommentLegacy( $db, 'ar_comment', $row, true );
-
- $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
-
- return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
- }
-
- /**
- * @see RevisionFactory::newRevisionFromRow
- *
- * MCR migration note: this replaces Revision::newFromRow
- *
- * @param object $row
- * @param int $queryFlags
- * @param Title|null $title
- *
- * @return RevisionRecord
- */
- public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
- Assert::parameterType( 'object', $row, '$row' );
-
- if ( !$title ) {
- $pageId = $row->rev_page ?? 0; // XXX: also check page_id?
- $revId = $row->rev_id ?? 0;
-
- $title = $this->getTitle( $pageId, $revId, $queryFlags );
- }
-
- 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() );
- }
- }
-
- try {
- $user = User::newFromAnyId(
- $row->rev_user ?? null,
- $row->rev_user_text ?? null,
- $row->rev_actor ?? null
- );
- } catch ( InvalidArgumentException $ex ) {
- wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
- $user = new UserIdentityValue( 0, 'Unknown user', 0 );
- }
-
- $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
- // Legacy because $row may have come from self::selectFields()
- $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
-
- $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
-
- return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
- }
-
- /**
- * 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 ) {
- $pageId = $fields['page'] ?? 0;
- $revId = $fields['id'] ?? 0;
-
- $title = $this->getTitle( $pageId, $revId, $queryFlags );
- }
-
- if ( !isset( $fields['page'] ) ) {
- $fields['page'] = $title->getArticleID( $queryFlags );
- }
-
- // if we have a content object, use it to set the model and type
- if ( !empty( $fields['content'] ) ) {
- if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
- throw new MWException(
- 'content field must contain a Content object or an array of Content objects.'
- );
- }
- }
-
- if ( !empty( $fields['text_id'] ) ) {
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
- throw new MWException( "The text_id field is only available in the pre-MCR schema" );
- }
-
- if ( !empty( $fields['content'] ) ) {
- throw new MWException(
- "Text already stored in external store (id {$fields['text_id']}), " .
- "can't specify content object"
- );
- }
- }
-
- if (
- isset( $fields['comment'] )
- && !( $fields['comment'] instanceof CommentStoreComment )
- ) {
- $commentData = $fields['comment_data'] ?? null;
-
- if ( $fields['comment'] instanceof Message ) {
- $fields['comment'] = CommentStoreComment::newUnsavedComment(
- $fields['comment'],
- $commentData
- );
- } else {
- $commentText = trim( strval( $fields['comment'] ) );
- $fields['comment'] = CommentStoreComment::newUnsavedComment(
- $commentText,
- $commentData
- );
- }
- }
-
- $revision = new MutableRevisionRecord( $title, $this->wikiId );
- $this->initializeMutableRevisionFromArray( $revision, $fields );
-
- if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
- foreach ( $fields['content'] as $role => $content ) {
- $revision->setContent( $role, $content );
- }
- } else {
- $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
- $revision->setSlot( $mainSlot );
- }
-
- return $revision;
- }
-
- /**
- * @param MutableRevisionRecord $record
- * @param array $fields
- */
- private function initializeMutableRevisionFromArray(
- MutableRevisionRecord $record,
- array $fields
- ) {
- /** @var UserIdentity $user */
- $user = null;
-
- if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
- $user = $fields['user'];
- } else {
- try {
- $user = User::newFromAnyId(
- $fields['user'] ?? null,
- $fields['user_text'] ?? null,
- $fields['actor'] ?? null
- );
- } catch ( InvalidArgumentException $ex ) {
- $user = null;
- }
- }
-
- if ( $user ) {
- $record->setUser( $user );
- }
-
- $timestamp = isset( $fields['timestamp'] )
- ? strval( $fields['timestamp'] )
- : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
-
- $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'] );
- }
- if ( isset( $fields['size'] ) ) {
- $record->setSize( intval( $fields['size'] ) );
- }
-
- 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 a page revision from a given revision ID number.
- * Returns null if no such revision can be found.
- *
- * MCR migration note: this corresponds to Revision::loadFromId
- *
- * @note direct use is deprecated!
- * @todo remove when unused! there seem to be no callers of Revision::loadFromId
- *
- * @param IDatabase $db
- * @param int $id
- *
- * @return RevisionRecord|null
- */
- public function loadRevisionFromId( IDatabase $db, $id ) {
- return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
- }
-
- /**
- * 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
- *
- * @note direct use is deprecated!
- * @todo remove when unused!
- *
- * @param IDatabase $db
- * @param int $pageid
- * @param int $id
- * @return RevisionRecord|null
- */
- public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
- $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!
- *
- * @param IDatabase $db
- * @param Title $title
- * @param int $id
- *
- * @return RevisionRecord|null
- */
- public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
- 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
- *
- * @note direct use is deprecated! Use getRevisionFromTimestamp instead!
- * @todo remove when unused!
- *
- * @param IDatabase $db
- * @param Title $title
- * @param string $timestamp
- * @return RevisionRecord|null
- */
- public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
- 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)
- * @param Title|null $title
- *
- * @return RevisionRecord|null
- */
- private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
- $db = $this->getDBConnectionRefForQueryFlags( $flags );
- $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
-
- $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 )
- && $lb->getServerCount() > 1
- && $lb->hasOrMadeRecentMasterChanges()
- ) {
- $flags = self::READ_LATEST;
- $dbw = $this->getDBConnection( DB_MASTER );
- $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $title );
- $this->releaseDBConnection( $dbw );
- }
-
- 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)
- * @param Title|null $title
- *
- * @return RevisionRecord|null
- */
- private function loadRevisionFromConds(
- IDatabase $db,
- $conditions,
- $flags = 0,
- Title $title = null
- ) {
- $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
- 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
- */
- private function checkDatabaseWikiId( IDatabase $db ) {
- $storeWiki = $this->wikiId;
- $dbWiki = $db->getDomainID();
-
- if ( $dbWiki === $storeWiki ) {
- return;
- }
-
- // XXX: we really want the default database ID...
- $storeWiki = $storeWiki ?: wfWikiID();
- $dbWiki = $dbWiki ?: wfWikiID();
-
- if ( $dbWiki === $storeWiki ) {
- return;
- }
-
- // HACK: counteract encoding imposed by DatabaseDomain
- $storeWiki = str_replace( '?h', '-', $storeWiki );
- $dbWiki = str_replace( '?h', '-', $dbWiki );
-
- if ( $dbWiki === $storeWiki ) {
- return;
- }
-
- throw new MWException( "RevisionStore for $storeWiki "
- . "cannot be used with a DB connection for $dbWiki" );
- }
-
- /**
- * 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)
- *
- * @return object|false data row as a raw object
- */
- private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
- $this->checkDatabaseWikiId( $db );
-
- $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
- $options = [];
- if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
- $options[] = 'FOR UPDATE';
- }
- return $db->selectRow(
- $revQuery['tables'],
- $revQuery['fields'],
- $conditions,
- __METHOD__,
- $options,
- $revQuery['joins']
- );
- }
-
- /**
- * Finds the ID of a content row for a given revision and slot role.
- * This can be used to re-use content rows even while the content ID
- * is still missing from SlotRecords, when writing to both the old and
- * the new schema during MCR schema migration.
- *
- * @todo remove after MCR schema migration is complete.
- *
- * @param IDatabase $db
- * @param int $revId
- * @param string $role
- *
- * @return int|null
- */
- private function findSlotContentId( IDatabase $db, $revId, $role ) {
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
- return null;
- }
-
- try {
- $roleId = $this->slotRoleStore->getId( $role );
- $conditions = [
- 'slot_revision_id' => $revId,
- 'slot_role_id' => $roleId,
- ];
-
- $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
-
- return $contentId ?: null;
- } catch ( NameTableAccessException $ex ) {
- // If the role is missing from the slot_roles table,
- // the corresponding row in slots cannot exist.
- return null;
- }
- }
-
- /**
- * Return the tables, fields, and join conditions to be selected to create
- * a new RevisionStoreRecord object.
- *
- * MCR migration note: this replaces Revision::getQueryInfo
- *
- * If the format of fields returned changes in any way then the cache key provided by
- * self::getRevisionRowCacheKey should be updated.
- *
- * @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
- * - 'text': Join with the text table, and select fields to load page text. This
- * option is deprecated in MW 1.32 when the MCR migration flag SCHEMA_COMPAT_WRITE_NEW
- * is set, and disallowed when SCHEMA_COMPAT_READ_OLD is not set.
- *
- * @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 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',
- ] );
-
- $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
- $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
- $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
- $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
-
- $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
- $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
- $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
- $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
-
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
- $ret['fields'][] = 'rev_text_id';
-
- if ( $this->contentHandlerUseDB ) {
- $ret['fields'][] = 'rev_content_format';
- $ret['fields'][] = 'rev_content_model';
- }
- }
-
- 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',
- ] );
- $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
- }
-
- if ( in_array( 'user', $options, true ) ) {
- $ret['tables'][] = 'user';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'user_name',
- ] );
- $u = $actorQuery['fields']['rev_user'];
- $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
- }
-
- if ( in_array( 'text', $options, true ) ) {
- if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
- throw new InvalidArgumentException( 'text table can no longer be joined directly' );
- } elseif ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
- // NOTE: even when this class is set to not read from the old schema, callers
- // should still be able to join against the text table, as long as we are still
- // writing the old schema for compatibility.
- // TODO: This should trigger a deprecation warning eventually (T200918), but not
- // before all known usages are removed (see T198341 and T201164).
- // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
- }
-
- $ret['tables'][] = 'text';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'old_text',
- 'old_flags'
- ] );
- $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
- }
-
- return $ret;
- }
-
- /**
- * Return the tables, fields, and join conditions to be selected to create
- * 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
- * - '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
- *
- * @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 getSlotsQueryInfo( $options = [] ) {
- $ret = [
- 'tables' => [],
- 'fields' => [],
- 'joins' => [],
- ];
-
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
- $db = $this->getDBConnectionRef( DB_REPLICA );
- $ret['tables']['slots'] = 'revision';
-
- $ret['fields']['slot_revision_id'] = 'slots.rev_id';
- $ret['fields']['slot_content_id'] = 'NULL';
- $ret['fields']['slot_origin'] = 'slots.rev_id';
- $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
-
- if ( in_array( 'content', $options, true ) ) {
- $ret['fields']['content_size'] = 'slots.rev_len';
- $ret['fields']['content_sha1'] = 'slots.rev_sha1';
- $ret['fields']['content_address']
- = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
-
- if ( $this->contentHandlerUseDB ) {
- $ret['fields']['model_name'] = 'slots.rev_content_model';
- } else {
- $ret['fields']['model_name'] = 'NULL';
- }
- }
- } else {
- $ret['tables'][] = 'slots';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'slot_revision_id',
- 'slot_content_id',
- 'slot_origin',
- 'slot_role_id',
- ] );
-
- 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';
- }
-
- if ( in_array( 'content', $options, true ) ) {
- $ret['tables'][] = 'content';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'content_size',
- 'content_sha1',
- 'content_address',
- 'content_model',
- ] );
- $ret['joins']['content'] = [ 'INNER 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';
- }
-
- }
- }
-
- return $ret;
- }
-
- /**
- * Return the tables, fields, and join conditions to be selected to create
- * a new RevisionArchiveRecord object.
- *
- * 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() {
- $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
- $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
- $ret = [
- 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
- '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',
- ] + $commentQuery['fields'] + $actorQuery['fields'],
- 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
- ];
-
- if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
- $ret['fields'][] = 'ar_text_id';
-
- if ( $this->contentHandlerUseDB ) {
- $ret['fields'][] = 'ar_content_format';
- $ret['fields'][] = 'ar_content_model';
- }
- }
-
- return $ret;
- }
-
- /**
- * Do a batched query for the sizes of a set of revisions.
- *
- * MCR migration note: this replaces Revision::getParentLengths
- *
- * @param int[] $revIds
- * @return int[] associative array mapping revision IDs from $revIds to the nominal size
- * of the corresponding revision.
- */
- public function getRevisionSizes( array $revIds ) {
- return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
- }
-
- /**
- * Do a batched query for the sizes of a set of revisions.
- *
- * MCR migration note: this replaces Revision::getParentLengths
- *
- * @deprecated use RevisionStore::getRevisionSizes instead.
- *
- * @param IDatabase $db
- * @param int[] $revIds
- * @return int[] associative array mapping revision IDs from $revIds to the nominal size
- * of the corresponding revision.
- */
- public function listRevisionSizes( IDatabase $db, array $revIds ) {
- $this->checkDatabaseWikiId( $db );
-
- $revLens = [];
- if ( !$revIds ) {
- return $revLens; // empty
- }
-
- $res = $db->select(
- 'revision',
- [ 'rev_id', 'rev_len' ],
- [ 'rev_id' => $revIds ],
- __METHOD__
- );
-
- foreach ( $res as $row ) {
- $revLens[$row->rev_id] = intval( $row->rev_len );
- }
-
- return $revLens;
- }
-
- /**
- * Get previous revision for this title
- *
- * MCR migration note: this replaces Revision::getPrevious
- *
- * @param RevisionRecord $rev
- * @param Title|null $title if known (optional)
- *
- * @return RevisionRecord|null
- */
- public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
- if ( $title === null ) {
- $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
- }
- $prev = $title->getPreviousRevisionID( $rev->getId() );
- if ( $prev ) {
- return $this->getRevisionByTitle( $title, $prev );
- }
- return null;
- }
-
- /**
- * Get next revision for this title
- *
- * MCR migration note: this replaces Revision::getNext
- *
- * @param RevisionRecord $rev
- * @param Title|null $title if known (optional)
- *
- * @return RevisionRecord|null
- */
- public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
- if ( $title === null ) {
- $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
- }
- $next = $title->getNextRevisionID( $rev->getId() );
- if ( $next ) {
- return $this->getRevisionByTitle( $title, $next );
- }
- return null;
- }
-
- /**
- * 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 ) {
- $this->checkDatabaseWikiId( $db );
-
- 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 );
- }
-
- /**
- * Get rev_timestamp from rev_id, without loading the rest of the row
- *
- * MCR migration note: this replaces Revision::getTimestampFromId
- *
- * @param Title $title
- * @param int $id
- * @param int $flags
- * @return string|bool False if not found
- */
- public function getTimestampFromId( $title, $id, $flags = 0 ) {
- $db = $this->getDBConnectionRefForQueryFlags( $flags );
-
- $conds = [ 'rev_id' => $id ];
- $conds['rev_page'] = $title->getArticleID();
- $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
-
- return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
- }
-
- /**
- * 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 ) {
- $this->checkDatabaseWikiId( $db );
-
- $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 ) {
- $this->checkDatabaseWikiId( $db );
-
- if ( !$userId ) {
- return false;
- }
-
- $revQuery = $this->getQueryInfo();
- $res = $db->select(
- $revQuery['tables'],
- [
- 'rev_user' => $revQuery['fields']['rev_user'],
- ],
- [
- 'rev_page' => $pageId,
- 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
- ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
- $revQuery['joins']
- );
- 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
- */
- public function getKnownCurrentRevision( Title $title, $revId ) {
- $db = $this->getDBConnectionRef( DB_REPLICA );
-
- $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;
- }
-
- $row = $this->cache->getWithSetCallback(
- // Page/rev IDs passed in from DB to reflect history merges
- $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
- WANObjectCache::TTL_WEEK,
- function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
- $setOpts += Database::getCacheSetOptions( $db );
-
- $conds = [
- 'rev_page' => intval( $pageId ),
- 'page_id' => intval( $pageId ),
- 'rev_id' => intval( $revId ),
- ];
-
- $row = $this->fetchRevisionRowFromConds( $db, $conds );
- return $row ?: false; // don't cache negatives
- }
- );
-
- // Reflect revision deletion and user renames
- if ( $row ) {
- return $this->newRevisionFromRow( $row, 0, $title );
- } else {
- return false;
- }
- }
-
- /**
- * 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
- );
- }
-
- // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
-
-}
+++ /dev/null
-<?php
-
-/**
- * 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
- */
-
-namespace MediaWiki\Storage;
-
-use ActorMigration;
-use CommentStore;
-use MediaWiki\Logger\Spi as LoggerSpi;
-use WANObjectCache;
-use Wikimedia\Assert\Assert;
-use Wikimedia\Rdbms\ILBFactory;
-
-/**
- * Factory service for RevisionStore instances. This allows RevisionStores to be created for
- * cross-wiki access.
- *
- * @warning Beware compatibility issues with schema migration in the context of cross-wiki access!
- * This class assumes that all wikis are at compatible migration stages for all relevant schemas.
- * Relevant schemas are: revision storage (MCR), the revision comment table, and the actor table.
- * Migration stages are compatible as long as a) there are no wikis in the cluster that only write
- * the old schema or b) there are no wikis that read only the new schema.
- *
- * @since 1.32
- */
-class RevisionStoreFactory {
-
- /** @var BlobStoreFactory */
- private $blobStoreFactory;
- /** @var ILBFactory */
- private $dbLoadBalancerFactory;
- /** @var WANObjectCache */
- private $cache;
- /** @var LoggerSpi */
- private $loggerProvider;
-
- /** @var CommentStore */
- private $commentStore;
- /** @var ActorMigration */
- private $actorMigration;
- /** @var int One of the MIGRATION_* constants */
- private $mcrMigrationStage;
- /**
- * @var bool
- * @see $wgContentHandlerUseDB
- */
- private $contentHandlerUseDB;
-
- /** @var NameTableStoreFactory */
- private $nameTables;
-
- /**
- * @param ILBFactory $dbLoadBalancerFactory
- * @param BlobStoreFactory $blobStoreFactory
- * @param NameTableStoreFactory $nameTables
- * @param WANObjectCache $cache
- * @param CommentStore $commentStore
- * @param ActorMigration $actorMigration
- * @param int $migrationStage
- * @param LoggerSpi $loggerProvider
- * @param bool $contentHandlerUseDB see {@link $wgContentHandlerUseDB}. Must be the same
- * for all wikis in the cluster. Will go away after MCR migration.
- */
- public function __construct(
- ILBFactory $dbLoadBalancerFactory,
- BlobStoreFactory $blobStoreFactory,
- NameTableStoreFactory $nameTables,
- WANObjectCache $cache,
- CommentStore $commentStore,
- ActorMigration $actorMigration,
- $migrationStage,
- LoggerSpi $loggerProvider,
- $contentHandlerUseDB
- ) {
- Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
- $this->dbLoadBalancerFactory = $dbLoadBalancerFactory;
- $this->blobStoreFactory = $blobStoreFactory;
- $this->nameTables = $nameTables;
- $this->cache = $cache;
- $this->commentStore = $commentStore;
- $this->actorMigration = $actorMigration;
- $this->mcrMigrationStage = $migrationStage;
- $this->loggerProvider = $loggerProvider;
- $this->contentHandlerUseDB = $contentHandlerUseDB;
- }
-
- /**
- * @since 1.32
- *
- * @param bool|string $wikiId false for the current domain / wikid
- *
- * @return RevisionStore for the given wikiId with all necessary services and a logger
- */
- public function getRevisionStore( $wikiId = false ) {
- Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
-
- $store = new RevisionStore(
- $this->dbLoadBalancerFactory->getMainLB( $wikiId ),
- $this->blobStoreFactory->newSqlBlobStore( $wikiId ),
- $this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore.
- $this->commentStore,
- $this->nameTables->getContentModels( $wikiId ),
- $this->nameTables->getSlotRoles( $wikiId ),
- $this->mcrMigrationStage,
- $this->actorMigration,
- $wikiId
- );
-
- $store->setLogger( $this->loggerProvider->getLogger( 'RevisionStore' ) );
- $store->setContentHandlerUseDB( $this->contentHandlerUseDB );
-
- return $store;
- }
-}
+++ /dev/null
-<?php
-/**
- * A RevisionRecord representing an existing revision persisted in the revision table.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\User\UserIdentity;
-use Title;
-use User;
-use Wikimedia\Assert\Assert;
-
-/**
- * A RevisionRecord representing an existing revision persisted in the revision table.
- * RevisionStoreRecord has no optional fields, getters will never return null.
- *
- * @since 1.31
- */
-class RevisionStoreRecord extends RevisionRecord {
-
- /** @var bool */
- protected $mCurrent = false;
-
- /**
- * @note Avoid calling this constructor directly. Use the appropriate methods
- * in RevisionStore instead.
- *
- * @param Title $title The title of the page this Revision is associated with.
- * @param UserIdentity $user
- * @param CommentStoreComment $comment
- * @param object $row A row from the revision table. Use RevisionStore::getQueryInfo() to build
- * a query that yields the required fields.
- * @param RevisionSlots $slots The slots of this revision.
- * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
- * or false for the local site.
- */
- function __construct(
- Title $title,
- UserIdentity $user,
- CommentStoreComment $comment,
- $row,
- RevisionSlots $slots,
- $wikiId = false
- ) {
- parent::__construct( $title, $slots, $wikiId );
- Assert::parameterType( 'object', $row, '$row' );
-
- $this->mId = intval( $row->rev_id );
- $this->mPageId = intval( $row->rev_page );
- $this->mComment = $comment;
-
- $timestamp = wfTimestamp( TS_MW, $row->rev_timestamp );
- Assert::parameter( is_string( $timestamp ), '$row->rev_timestamp', 'must be a valid timestamp' );
-
- $this->mUser = $user;
- $this->mMinorEdit = boolval( $row->rev_minor_edit );
- $this->mTimestamp = $timestamp;
- $this->mDeleted = intval( $row->rev_deleted );
-
- // NOTE: rev_parent_id = 0 indicates that there is no parent revision, while null
- // indicates that the parent revision is unknown. As per MW 1.31, the database schema
- // allows rev_parent_id to be NULL.
- $this->mParentId = isset( $row->rev_parent_id ) ? intval( $row->rev_parent_id ) : null;
- $this->mSize = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
- $this->mSha1 = !empty( $row->rev_sha1 ) ? $row->rev_sha1 : null;
-
- // NOTE: we must not call $this->mTitle->getLatestRevID() here, since the state of
- // page_latest may be in limbo during revision creation. In that case, calling
- // $this->mTitle->getLatestRevID() would cause a bad value to be cached in the Title
- // object. During page creation, that bad value would be 0.
- if ( isset( $row->page_latest ) ) {
- $this->mCurrent = ( $row->rev_id == $row->page_latest );
- }
-
- // sanity check
- if (
- $this->mPageId && $this->mTitle->exists()
- && $this->mPageId !== $this->mTitle->getArticleID()
- ) {
- throw new InvalidArgumentException(
- 'The given Title does not belong to page ID ' . $this->mPageId .
- ' but actually belongs to ' . $this->mTitle->getArticleID()
- );
- }
- }
-
- /**
- * MCR migration note: this replaces Revision::isCurrent
- *
- * @return bool
- */
- public function isCurrent() {
- return $this->mCurrent;
- }
-
- /**
- * MCR migration note: this replaces Revision::isDeleted
- *
- * @param int $field One of DELETED_* bitfield constants
- *
- * @return bool
- */
- public function isDeleted( $field ) {
- if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
- // Current revisions of pages cannot have the content hidden. Skipping this
- // check is very useful for Parser as it fetches templates using newKnownCurrent().
- // Calling getVisibility() in that case triggers a verification database query.
- return false; // no need to check
- }
-
- return parent::isDeleted( $field );
- }
-
- protected function userCan( $field, User $user ) {
- if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
- // Current revisions of pages cannot have the content hidden. Skipping this
- // check is very useful for Parser as it fetches templates using newKnownCurrent().
- // Calling getVisibility() in that case triggers a verification database query.
- return true; // no need to check
- }
-
- return parent::userCan( $field, $user );
- }
-
- /**
- * @return int The revision id, never null.
- */
- public function getId() {
- // overwritten just to add a guarantee to the contract
- return parent::getId();
- }
-
- /**
- * @throws RevisionAccessException if the size was unknown and could not be calculated.
- * @return string The nominal revision size, never null. May be computed on the fly.
- */
- public function getSize() {
- // If length is null, calculate and remember it (potentially SLOW!).
- // This is for compatibility with old database rows that don't have the field set.
- if ( $this->mSize === null ) {
- $this->mSize = $this->mSlots->computeSize();
- }
-
- return $this->mSize;
- }
-
- /**
- * @throws RevisionAccessException if the hash was unknown and could not be calculated.
- * @return string The revision hash, never null. May be computed on the fly.
- */
- public function getSha1() {
- // If hash is null, calculate it and remember (potentially SLOW!)
- // This is for compatibility with old database rows that don't have the field set.
- if ( $this->mSha1 === null ) {
- $this->mSha1 = $this->mSlots->computeSha1();
- }
-
- return $this->mSha1;
- }
-
- /**
- * @param int $audience
- * @param User|null $user
- *
- * @return UserIdentity The identity of the revision author, null if access is forbidden.
- */
- public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- // overwritten just to add a guarantee to the contract
- return parent::getUser( $audience, $user );
- }
-
- /**
- * @param int $audience
- * @param User|null $user
- *
- * @return CommentStoreComment The revision comment, null if access is forbidden.
- */
- public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- // overwritten just to add a guarantee to the contract
- return parent::getComment( $audience, $user );
- }
-
- /**
- * @return string timestamp, never null
- */
- public function getTimestamp() {
- // overwritten just to add a guarantee to the contract
- return parent::getTimestamp();
- }
-
- /**
- * @see RevisionStore::isComplete
- *
- * @return bool always true.
- */
- public function isReadyForInsertion() {
- return true;
- }
-
-}
+++ /dev/null
-<?php
-/**
- * Value object representing a content slot associated with a page revision.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-use Content;
-use InvalidArgumentException;
-use LogicException;
-use OutOfBoundsException;
-use Wikimedia\Assert\Assert;
-
-/**
- * Value object representing a content slot associated with a page revision.
- * SlotRecord provides direct access to a Content object.
- * That access may be implemented through a callback.
- *
- * @since 1.31
- */
-class SlotRecord {
-
- const MAIN = 'main';
-
- /**
- * @var object database result row, as a raw object. Callbacks are supported for field values,
- * to enable on-demand emulation of these values. This is primarily intended for use
- * during schema migration.
- */
- private $row;
-
- /**
- * @var Content|callable
- */
- private $content;
-
- /**
- * Returns a new SlotRecord just like the given $slot, except that calling getContent()
- * will fail with an exception.
- *
- * @param SlotRecord $slot
- *
- * @return SlotRecord
- */
- public static function newWithSuppressedContent( SlotRecord $slot ) {
- $row = $slot->row;
-
- return new SlotRecord( $row, function () {
- throw new SuppressedDataException( 'Content suppressed!' );
- } );
- }
-
- /**
- * Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
- * The slot's content cannot be overwritten.
- *
- * @param SlotRecord $slot
- * @param array $overrides
- *
- * @return SlotRecord
- */
- private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
- $row = clone $slot->row;
- $row->slot_id = null; // never copy the row ID!
-
- foreach ( $overrides as $key => $value ) {
- $row->$key = $value;
- }
-
- return new SlotRecord( $row, $slot->content );
- }
-
- /**
- * Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord
- * of a previous revision.
- *
- * Note that a SlotRecord constructed this way are intended as prototypes,
- * to be used wit newSaved(). They are incomplete, so some getters such as
- * getRevision() will fail.
- *
- * @param SlotRecord $slot
- *
- * @return SlotRecord
- */
- public static function newInherited( SlotRecord $slot ) {
- // Sanity check - we can't inherit from a Slot that's not attached to a revision.
- $slot->getRevision();
- $slot->getOrigin();
- $slot->getAddress();
-
- // NOTE: slot_origin and content_address are copied from $slot.
- return self::newDerived( $slot, [
- 'slot_revision_id' => null,
- ] );
- }
-
- /**
- * Constructs a new Slot from a Content object for a new revision.
- * This is the preferred way to construct a slot for storing Content that
- * resulted from a user edit. The slot is assumed to be not inherited.
- *
- * Note that a SlotRecord constructed this way are intended as prototypes,
- * to be used wit newSaved(). They are incomplete, so some getters such as
- * getAddress() will fail.
- *
- * @param string $role
- * @param Content $content
- *
- * @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
- */
- public static function newUnsaved( $role, Content $content ) {
- Assert::parameterType( 'string', $role, '$role' );
-
- $row = [
- 'slot_id' => null, // not yet known
- 'slot_revision_id' => null, // not yet known
- 'slot_origin' => null, // not yet known, will be set in newSaved()
- 'content_size' => null, // compute later
- 'content_sha1' => null, // compute later
- 'slot_content_id' => null, // not yet known, will be set in newSaved()
- 'content_address' => null, // not yet known, will be set in newSaved()
- 'role_name' => $role,
- 'model_name' => $content->getModel(),
- ];
-
- return new SlotRecord( (object)$row, $content );
- }
-
- /**
- * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
- * proto-slot. This adds information that has only become available during saving,
- * particularly the revision ID, content ID and content address.
- *
- * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
- * If $protoSlot already has a revision, it must be the same.
- * @param int|null $contentId the ID of the row in the content table describing the content
- * referenced by $contentAddress (field slot_content_id).
- * If $protoSlot already has a content ID, it must be the same.
- * @param string $contentAddress the slot's content address (field content_address).
- * If $protoSlot already has an address, it must be the same.
- * @param SlotRecord $protoSlot The proto-slot that was provided as input for creating a new
- * revision. $protoSlot must have a content address if inherited.
- *
- * @return SlotRecord If the state of $protoSlot is inappropriate for saving a new revision.
- */
- public static function newSaved(
- $revisionId,
- $contentId,
- $contentAddress,
- SlotRecord $protoSlot
- ) {
- Assert::parameterType( 'integer', $revisionId, '$revisionId' );
- // TODO once migration is over $contentId must be an integer
- Assert::parameterType( 'integer|null', $contentId, '$contentId' );
- Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
-
- if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
- throw new LogicException(
- "Mismatching revision ID $revisionId: "
- . "The slot already belongs to revision {$protoSlot->getRevision()}. "
- . "Use SlotRecord::newInherited() to re-use content between revisions."
- );
- }
-
- if ( $protoSlot->hasAddress() && $protoSlot->getAddress() !== $contentAddress ) {
- throw new LogicException(
- "Mismatching blob address $contentAddress: "
- . "The slot already has content at {$protoSlot->getAddress()}."
- );
- }
-
- if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
- throw new LogicException(
- "Mismatching content ID $contentId: "
- . "The slot already has content row {$protoSlot->getContentId()} associated."
- );
- }
-
- if ( $protoSlot->isInherited() ) {
- if ( !$protoSlot->hasAddress() ) {
- throw new InvalidArgumentException(
- "An inherited blob should have a content address!"
- );
- }
- if ( !$protoSlot->hasField( 'slot_origin' ) ) {
- throw new InvalidArgumentException(
- "A saved inherited slot should have an origin set!"
- );
- }
- $origin = $protoSlot->getOrigin();
- } else {
- $origin = $revisionId;
- }
-
- return self::newDerived( $protoSlot, [
- 'slot_revision_id' => $revisionId,
- 'slot_content_id' => $contentId,
- 'slot_origin' => $origin,
- 'content_address' => $contentAddress,
- ] );
- }
-
- /**
- * SlotRecord constructor.
- *
- * The following fields are supported by the $row parameter:
- *
- * $row->blob_data
- * $row->blob_address
- *
- * @param object $row A database row composed of fields of the slot and content tables,
- * as a raw object. Any field value can be a callback that produces the field value
- * given this SlotRecord as a parameter. However, plain strings cannot be used as
- * callbacks here, for security reasons.
- * @param Content|callable $content The content object associated with the slot, or a
- * callback that will return that Content object, given this SlotRecord as a parameter.
- */
- public function __construct( $row, $content ) {
- Assert::parameterType( 'object', $row, '$row' );
- Assert::parameterType( 'Content|callable', $content, '$content' );
-
- Assert::parameter(
- property_exists( $row, 'slot_revision_id' ),
- '$row->slot_revision_id',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'slot_content_id' ),
- '$row->slot_content_id',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'content_address' ),
- '$row->content_address',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'model_name' ),
- '$row->model_name',
- 'must exist'
- );
- Assert::parameter(
- property_exists( $row, 'slot_origin' ),
- '$row->slot_origin',
- 'must exist'
- );
- Assert::parameter(
- !property_exists( $row, 'slot_inherited' ),
- '$row->slot_inherited',
- 'must not exist'
- );
- Assert::parameter(
- !property_exists( $row, 'slot_revision' ),
- '$row->slot_revision',
- 'must not exist'
- );
-
- $this->row = $row;
- $this->content = $content;
- }
-
- /**
- * Implemented to defy serialization.
- *
- * @throws LogicException always
- */
- public function __sleep() {
- throw new LogicException( __CLASS__ . ' is not serializable.' );
- }
-
- /**
- * Returns the Content of the given slot.
- *
- * @note This is free to load Content from whatever subsystem is necessary,
- * performing potentially expensive operations and triggering I/O-related
- * failure modes.
- *
- * @note This method does not apply audience filtering.
- *
- * @throws SuppressedDataException if access to the content is not allowed according
- * to the audience check performed by RevisionRecord::getSlot().
- *
- * @return Content The slot's content. This is a direct reference to the internal instance,
- * copy before exposing to application logic!
- */
- public function getContent() {
- if ( $this->content instanceof Content ) {
- return $this->content;
- }
-
- $obj = call_user_func( $this->content, $this );
-
- Assert::postcondition(
- $obj instanceof Content,
- 'Slot content callback should return a Content object'
- );
-
- $this->content = $obj;
-
- return $this->content;
- }
-
- /**
- * Returns the string value of a data field from the database row supplied to the constructor.
- * If the field was set to a callback, that callback is invoked and the result returned.
- *
- * @param string $name
- *
- * @throws OutOfBoundsException
- * @throws IncompleteRevisionException
- * @return mixed Returns the field's value, never null.
- */
- private function getField( $name ) {
- if ( !isset( $this->row->$name ) ) {
- // distinguish between unknown and uninitialized fields
- if ( property_exists( $this->row, $name ) ) {
- throw new IncompleteRevisionException( 'Uninitialized field: ' . $name );
- } else {
- throw new OutOfBoundsException( 'No such field: ' . $name );
- }
- }
-
- $value = $this->row->$name;
-
- // NOTE: allow callbacks, but don't trust plain string callables from the database!
- if ( !is_string( $value ) && is_callable( $value ) ) {
- $value = call_user_func( $value, $this );
- $this->setField( $name, $value );
- }
-
- return $value;
- }
-
- /**
- * Returns the string value of a data field from the database row supplied to the constructor.
- *
- * @param string $name
- *
- * @throws OutOfBoundsException
- * @throws IncompleteRevisionException
- * @return string Returns the string value
- */
- private function getStringField( $name ) {
- return strval( $this->getField( $name ) );
- }
-
- /**
- * Returns the int value of a data field from the database row supplied to the constructor.
- *
- * @param string $name
- *
- * @throws OutOfBoundsException
- * @throws IncompleteRevisionException
- * @return int Returns the int value
- */
- private function getIntField( $name ) {
- return intval( $this->getField( $name ) );
- }
-
- /**
- * @param string $name
- * @return bool whether this record contains the given field
- */
- private function hasField( $name ) {
- if ( isset( $this->row->$name ) ) {
- // if the field is a callback, resolve first, then re-check
- if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
- $this->getField( $name );
- }
- }
-
- return isset( $this->row->$name );
- }
-
- /**
- * Returns the ID of the revision this slot is associated with.
- *
- * @return int
- */
- public function getRevision() {
- return $this->getIntField( 'slot_revision_id' );
- }
-
- /**
- * Returns the revision ID of the revision that originated the slot's content.
- *
- * @return int
- */
- public function getOrigin() {
- return $this->getIntField( 'slot_origin' );
- }
-
- /**
- * Whether this slot was inherited from an older revision.
- *
- * If this SlotRecord is already attached to a revision, this returns true
- * if the slot's revision of origin is the same as the revision it belongs to.
- *
- * If this SlotRecord is not yet attached to a revision, this returns true
- * if the slot already has an address.
- *
- * @return bool
- */
- public function isInherited() {
- if ( $this->hasRevision() ) {
- return $this->getRevision() !== $this->getOrigin();
- } else {
- return $this->hasAddress();
- }
- }
-
- /**
- * Whether this slot has an address. Slots will have an address if their
- * content has been stored. While building a new revision,
- * SlotRecords will not have an address associated.
- *
- * @return bool
- */
- public function hasAddress() {
- return $this->hasField( 'content_address' );
- }
-
- /**
- * Whether this slot has an origin (revision ID that originated the slot's content.
- *
- * @since 1.32
- *
- * @return bool
- */
- public function hasOrigin() {
- return $this->hasField( 'slot_origin' );
- }
-
- /**
- * Whether this slot has a content ID. Slots will have a content ID if their
- * content has been stored in the content table. While building a new revision,
- * SlotRecords will not have an ID associated.
- *
- * Also, during schema migration, hasContentId() may return false when encountering an
- * un-migrated database entry in SCHEMA_COMPAT_WRITE_BOTH mode.
- * It will however always return true for saved revisions on SCHEMA_COMPAT_READ_NEW mode,
- * or without SCHEMA_COMPAT_WRITE_NEW mode. In the latter case, an emulated content ID
- * is used, derived from the revision's text ID.
- *
- * Note that hasContentId() returning false while hasRevision() returns true always
- * indicates an unmigrated row in SCHEMA_COMPAT_WRITE_BOTH mode, as described above.
- * For an unsaved slot, both these methods would return false.
- *
- * @since 1.32
- *
- * @return bool
- */
- public function hasContentId() {
- return $this->hasField( 'slot_content_id' );
- }
-
- /**
- * Whether this slot has revision ID associated. Slots will have a revision ID associated
- * only if they were loaded as part of an existing revision. While building a new revision,
- * Slotrecords will not have a revision ID associated.
- *
- * @return bool
- */
- public function hasRevision() {
- return $this->hasField( 'slot_revision_id' );
- }
-
- /**
- * Returns the role of the slot.
- *
- * @return string
- */
- public function getRole() {
- return $this->getStringField( 'role_name' );
- }
-
- /**
- * Returns the address of this slot's content.
- * This address can be used with BlobStore to load the Content object.
- *
- * @return string
- */
- public function getAddress() {
- return $this->getStringField( 'content_address' );
- }
-
- /**
- * Returns the ID of the content meta data row associated with the slot.
- * This information should be irrelevant to application logic, it is here to allow
- * the construction of a full row for the revision table.
- *
- * Note that this method may return an emulated value during schema migration in
- * SCHEMA_COMPAT_WRITE_OLD mode. See RevisionStore::emulateContentId for more information.
- *
- * @return int
- */
- public function getContentId() {
- return $this->getIntField( 'slot_content_id' );
- }
-
- /**
- * Returns the content size
- *
- * @return int size of the content, in bogo-bytes, as reported by Content::getSize.
- */
- public function getSize() {
- try {
- $size = $this->getIntField( 'content_size' );
- } catch ( IncompleteRevisionException $ex ) {
- $size = $this->getContent()->getSize();
- $this->setField( 'content_size', $size );
- }
-
- return $size;
- }
-
- /**
- * Returns the content size
- *
- * @return string hash of the content.
- */
- public function getSha1() {
- try {
- $sha1 = $this->getStringField( 'content_sha1' );
- } catch ( IncompleteRevisionException $ex ) {
- $format = $this->hasField( 'format_name' )
- ? $this->getStringField( 'format_name' )
- : null;
-
- $data = $this->getContent()->serialize( $format );
- $sha1 = self::base36Sha1( $data );
- $this->setField( 'content_sha1', $sha1 );
- }
-
- return $sha1;
- }
-
- /**
- * Returns the content model. This is the model name that decides
- * which ContentHandler is appropriate for interpreting the
- * data of the blob referenced by the address returned by getAddress().
- *
- * @return string the content model of the content
- */
- public function getModel() {
- try {
- $model = $this->getStringField( 'model_name' );
- } catch ( IncompleteRevisionException $ex ) {
- $model = $this->getContent()->getModel();
- $this->setField( 'model_name', $model );
- }
-
- return $model;
- }
-
- /**
- * Returns the blob serialization format as a MIME type.
- *
- * @note When this method returns null, the caller is expected
- * to auto-detect the serialization format, or to rely on
- * the default format associated with the content model.
- *
- * @return string|null
- */
- public function getFormat() {
- // XXX: we currently do not plan to store the format for each slot!
-
- if ( $this->hasField( 'format_name' ) ) {
- return $this->getStringField( 'format_name' );
- }
-
- return null;
- }
-
- /**
- * @param string $name
- * @param string|int|null $value
- */
- private function setField( $name, $value ) {
- $this->row->$name = $value;
- }
-
- /**
- * Get the base 36 SHA-1 value for a string of text
- *
- * MCR migration note: this replaces Revision::base36Sha1
- *
- * @param string $blob
- * @return string
- */
- public static function base36Sha1( $blob ) {
- return \Wikimedia\base_convert( sha1( $blob ), 16, 36, 31 );
- }
-
- /**
- * Returns true if $other has the same content as this slot.
- * The check is performed based on the model, address size, and hash.
- * Two slots can have the same content if they use different content addresses,
- * but if they have the same address and the same model, they have the same content.
- * Two slots can have the same content if they belong to different
- * revisions or pages.
- *
- * Note that hasSameContent() may return false even if Content::equals returns true for
- * the content of two slots. This may happen if the two slots have different serializations
- * representing equivalent Content. Such false negatives are considered acceptable. Code
- * that has to be absolutely sure the Content is really not the same if hasSameContent()
- * returns false should call getContent() and compare the Content objects directly.
- *
- * @since 1.32
- *
- * @param SlotRecord $other
- * @return bool
- */
- public function hasSameContent( SlotRecord $other ) {
- if ( $other === $this ) {
- return true;
- }
-
- if ( $this->getModel() !== $other->getModel() ) {
- return false;
- }
-
- if ( $this->hasAddress()
- && $other->hasAddress()
- && $this->getAddress() == $other->getAddress()
- ) {
- return true;
- }
-
- if ( $this->getSize() !== $other->getSize() ) {
- return false;
- }
-
- if ( $this->getSha1() !== $other->getSha1() ) {
- return false;
- }
-
- return true;
- }
-
-}
+++ /dev/null
-<?php
-/**
- * Exception representing a failure to look up a revision.
- *
- * 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
- *
- * @file
- */
-
-namespace MediaWiki\Storage;
-
-/**
- * Exception raised in response to an audience check when attempting to
- * access suppressed information without permission.
- *
- * @since 1.31
- */
-class SuppressedDataException extends RevisionAccessException {
-
-}
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
/**
* Temporary action for MCR undos
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
class ApiComparePages extends ApiBase {
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
/**
* @ingroup API
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
/**
* Query module to enumerate all deleted revisions.
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
/**
* Query module to enumerate all revisions.
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
/**
* A query module to show contributors to a page
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
/**
* Query module to enumerate deleted revisions for pages.
* @file
*/
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
/**
* Query module to enumerate all deleted files.
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
/**
* A query action to enumerate the recent changes that were done to the wiki.
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
/**
* A query action to enumerate revisions of a given page, or show top revisions
* @file
*/
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\MediaWikiServices;
/**
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
-use MediaWiki\Storage\RevisionRecord;
/**
* This query action adds a list of a specified user's contributions to the output.
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
/**
* This query action allows clients to retrieve a list of recently modified pages
* @since 1.23
*/
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
/**
* API interface to RevDel. The API equivalent of Special:RevisionDelete.
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Revision\RevisionStore;
/**
* @ingroup API
* @ingroup DifferenceEngine
*/
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
/**
* DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
* @ingroup JobQueue
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
/**
* Job to update link tables for pages
* @file
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
/**
* Class for viewing MediaWiki article and history.
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
use MediaWiki\Storage\SqlBlobStore;
use Wikimedia\Assert\Assert;
use Wikimedia\Rdbms\IResultWrapper;
use MediaWiki\Edit\PreparedEdit;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionRenderer;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\DerivedPageDataUpdater;
use MediaWiki\Storage\PageUpdater;
-use MediaWiki\Storage\RevisionRecord;
use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
use Wikimedia\Assert\Assert;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionRenderer;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
class PoolWorkArticleView extends PoolCounterWork {
/** @var WikiPage */
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
use Wikimedia\Rdbms\IResultWrapper;
/**
* @ingroup Maintenance
*/
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
require_once __DIR__ . '/Maintenance.php';
*/
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\NameTableStore;
-use MediaWiki\Storage\SlotRecord;
use MediaWiki\Storage\SqlBlobStore;
use Wikimedia\Assert\Assert;
use Wikimedia\Rdbms\IDatabase;
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
require_once __DIR__ . '/../Maintenance.php';
'SpecialPageTestBase' => "$testDir/phpunit/includes/specials/SpecialPageTestBase.php",
'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php",
- # tests/phpunit/includes/Storage
- 'MediaWiki\Tests\Storage\McrSchemaDetection' => "$testDir/phpunit/includes/Storage/McrSchemaDetection.php",
- 'MediaWiki\Tests\Storage\McrSchemaOverride' => "$testDir/phpunit/includes/Storage/McrSchemaOverride.php",
- 'MediaWiki\Tests\Storage\McrWriteBothSchemaOverride' => "$testDir/phpunit/includes/Storage/McrWriteBothSchemaOverride.php",
- 'MediaWiki\Tests\Storage\McrReadNewSchemaOverride' => "$testDir/phpunit/includes/Storage/McrReadNewSchemaOverride.php",
- 'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php",
- 'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php",
- 'MediaWiki\Tests\Storage\RevisionStoreDbTestBase' => "$testDir/phpunit/includes/Storage/RevisionStoreDbTestBase.php",
- 'MediaWiki\Tests\Storage\PreMcrSchemaOverride' => "$testDir/phpunit/includes/Storage/PreMcrSchemaOverride.php",
+ # tests/phpunit/includes/Revision
+ 'MediaWiki\Tests\Revision\McrSchemaDetection' => "$testDir/phpunit/includes/Revision/McrSchemaDetection.php",
+ 'MediaWiki\Tests\Revision\McrSchemaOverride' => "$testDir/phpunit/includes/Revision/McrSchemaOverride.php",
+ 'MediaWiki\Tests\Revision\McrWriteBothSchemaOverride' => "$testDir/phpunit/includes/Revision/McrWriteBothSchemaOverride.php",
+ 'MediaWiki\Tests\Revision\McrReadNewSchemaOverride' => "$testDir/phpunit/includes/Revision/McrReadNewSchemaOverride.php",
+ 'MediaWiki\Tests\Revision\RevisionSlotsTest' => "$testDir/phpunit/includes/Revision/RevisionSlotsTest.php",
+ 'MediaWiki\Tests\Revision\RevisionRecordTests' => "$testDir/phpunit/includes/Revision/RevisionRecordTests.php",
+ 'MediaWiki\Tests\Revision\RevisionStoreDbTestBase' => "$testDir/phpunit/includes/Revision/RevisionStoreDbTestBase.php",
+ 'MediaWiki\Tests\Revision\PreMcrSchemaOverride' => "$testDir/phpunit/includes/Revision/PreMcrSchemaOverride.php",
# tests/phpunit/languages
'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php",
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use TextContent;
+use Title;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrReadNewRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+ use McrReadNewSchemaOverride;
+
+ protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+ $numberOfSlots = count( $rev->getSlotRoles() );
+
+ // new schema is written
+ $this->assertSelect(
+ 'slots',
+ [ 'count(*)' ],
+ [ 'slot_revision_id' => $rev->getId() ],
+ [ [ (string)$numberOfSlots ] ]
+ );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
+
+ $this->assertSelect(
+ $revQuery['tables'],
+ [ 'count(*)' ],
+ [
+ 'slot_revision_id' => $rev->getId(),
+ ],
+ [ [ (string)$numberOfSlots ] ],
+ [],
+ $revQuery['joins']
+ );
+
+ // Legacy schema is still being written
+ $this->assertSelect(
+ [ 'revision', 'text' ],
+ [ 'count(*)' ],
+ [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
+ [ [ 1 ] ],
+ [],
+ [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+ );
+
+ parent::assertRevisionExistsInDatabase( $rev );
+ }
+
+ /**
+ * @param SlotRecord $a
+ * @param SlotRecord $b
+ */
+ protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+ parent::assertSameSlotContent( $a, $b );
+
+ // Assert that the same content ID has been used
+ $this->assertSame( $a->getContentId(), $b->getContentId() );
+ }
+
+ public function provideInsertRevisionOn_successes() {
+ foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
+ yield $case;
+ }
+
+ yield 'Multi-slot revision insertion' => [
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'aux' => new TextContent( 'Egg' ),
+ ],
+ 'page' => true,
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ ];
+ }
+
+ public function provideNewNullRevision() {
+ foreach ( parent::provideNewNullRevision() as $case ) {
+ yield $case;
+ }
+
+ yield [
+ Title::newFromText( 'UTPage_notAutoCreated' ),
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'aux' => new WikitextContent( 'Omelet' ),
+ ],
+ ],
+ CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
+ ];
+ }
+
+ public function testGetQueryInfo_NoSlotDataJoin() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $queryInfo = $store->getQueryInfo();
+
+ // with the new schema enabled, query info should not join the main slot info
+ $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
+ $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, multiple roles' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 29,
+ 'parent_id' => 1,
+ 'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
+ 'comment' => 'Goat Comment!',
+ 'content' => [
+ 'main' => new WikitextContent( 'Söme Cöntent' ),
+ 'aux' => new TextContent( 'Öther Cöntent' ),
+ ]
+ ]
+ ];
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the intermediate MCR database
+ * schema for use during schema migration.
+ */
+trait McrReadNewSchemaOverride {
+
+ use PatchFileLocation;
+ use McrSchemaDetection;
+
+ /**
+ * @return int
+ */
+ protected function getMcrMigrationStage() {
+ return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
+ }
+
+ /**
+ * @return string[]
+ */
+ protected function getMcrTablesToReset() {
+ return [ 'content', 'content_models', 'slots', 'slot_roles' ];
+ }
+
+ /**
+ * @return array[]
+ */
+ protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+ $overrides = [
+ 'scripts' => [],
+ 'drop' => [],
+ 'create' => [],
+ 'alter' => [],
+ ];
+
+ if ( !$this->hasMcrTables( $db ) ) {
+ $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
+ }
+
+ if ( !$this->hasPreMcrFields( $db ) ) {
+ $overrides['alter'][] = 'revision';
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
+ }
+
+ return $overrides;
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use TextContent;
+use Title;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the post-migration MCR DB schema.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+ use McrSchemaOverride;
+
+ protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+ $numberOfSlots = count( $rev->getSlotRoles() );
+
+ // new schema is written
+ $this->assertSelect(
+ 'slots',
+ [ 'count(*)' ],
+ [ 'slot_revision_id' => $rev->getId() ],
+ [ [ (string)$numberOfSlots ] ]
+ );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
+
+ $this->assertSelect(
+ $revQuery['tables'],
+ [ 'count(*)' ],
+ [
+ 'slot_revision_id' => $rev->getId(),
+ ],
+ [ [ (string)$numberOfSlots ] ],
+ [],
+ $revQuery['joins']
+ );
+
+ parent::assertRevisionExistsInDatabase( $rev );
+ }
+
+ /**
+ * @param SlotRecord $a
+ * @param SlotRecord $b
+ */
+ protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+ parent::assertSameSlotContent( $a, $b );
+
+ // Assert that the same content ID has been used
+ $this->assertSame( $a->getContentId(), $b->getContentId() );
+ }
+
+ public function provideInsertRevisionOn_successes() {
+ foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
+ yield $case;
+ }
+
+ yield 'Multi-slot revision insertion' => [
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'aux' => new TextContent( 'Egg' ),
+ ],
+ 'page' => true,
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ ];
+ }
+
+ public function provideNewNullRevision() {
+ foreach ( parent::provideNewNullRevision() as $case ) {
+ yield $case;
+ }
+
+ yield [
+ Title::newFromText( 'UTPage_notAutoCreated' ),
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'aux' => new WikitextContent( 'Omelet' ),
+ ],
+ ],
+ CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, multiple roles' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 29,
+ 'parent_id' => 1,
+ 'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
+ 'comment' => 'Goat Comment!',
+ 'content' => [
+ 'main' => new WikitextContent( 'Söme Cöntent' ),
+ 'aux' => new TextContent( 'Öther Cöntent' ),
+ ]
+ ]
+ ];
+ }
+
+ public function testGetQueryInfo_NoSlotDataJoin() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $queryInfo = $store->getQueryInfo();
+
+ // with the new schema enabled, query info should not join the main slot info
+ $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
+ $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+ * @covers \MediaWiki\Revision\RevisionStore::insertSlotRowOn
+ * @covers \MediaWiki\Revision\RevisionStore::insertContentRowOn
+ */
+ public function testInsertRevisionOn_T202032() {
+ // This test only makes sense for MySQL
+ if ( $this->db->getType() !== 'mysql' ) {
+ $this->assertTrue( true );
+ return;
+ }
+
+ // NOTE: must be done before checking MAX(rev_id)
+ $page = $this->getTestPage();
+
+ $maxRevId = $this->db->selectField( 'revision', 'MAX(rev_id)' );
+
+ // Construct a slot row that will conflict with the insertion of the next revision ID,
+ // to emulate the failure mode described in T202032. Nothing will ever read this row,
+ // we just need it to trigger a primary key conflict.
+ $this->db->insert( 'slots', [
+ 'slot_revision_id' => $maxRevId + 1,
+ 'slot_role_id' => 1,
+ 'slot_content_id' => 0,
+ 'slot_origin' => 0
+ ], __METHOD__ );
+
+ $rev = new MutableRevisionRecord( $page->getTitle() );
+ $rev->setTimestamp( '20180101000000' );
+ $rev->setComment( CommentStoreComment::newUnsavedComment( 'test' ) );
+ $rev->setUser( $this->getTestUser()->getUser() );
+ $rev->setContent( 'main', new WikitextContent( 'Text' ) );
+ $rev->setPageId( $page->getId() );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $return = $store->insertRevisionOn( $rev, $this->db );
+
+ $this->assertSame( $maxRevId + 2, $return->getId() );
+
+ // is the new revision correct?
+ $this->assertRevisionCompleteness( $return );
+ $this->assertRevisionRecordsEqual( $rev, $return );
+
+ // can we find it directly in the database?
+ $this->assertRevisionExistsInDatabase( $return );
+
+ // can we load it from the store?
+ $loaded = $store->getRevisionById( $return->getId() );
+ $this->assertRevisionCompleteness( $loaded );
+ $this->assertRevisionRecordsEqual( $return, $loaded );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Trait providing methods for detecting which MCR schema migration phase the current schema
+ * is compatible with.
+ */
+trait McrSchemaDetection {
+
+ /**
+ * Returns true if MCR-related tables exist in the database.
+ * If yes, the database is compatible with with MIGRATION_NEW.
+ * If hasPreMcrFields() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
+ *
+ * @param IDatabase $db
+ * @return bool
+ */
+ protected function hasMcrTables( IDatabase $db ) {
+ return $db->tableExists( 'slots', __METHOD__ );
+ }
+
+ /**
+ * Returns true if pre-MCR fields still exist in the database.
+ * If yes, the database is compatible with with MIGRATION_OLD mode.
+ * If hasMcrTables() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
+ *
+ * Note that if the database has been updated in MIGRATION_NEW mode,
+ * the rev_text_id field will be 0 for new revisions. This means that
+ * in MIGRATION_OLD mode, reading such revisions will fail, even though
+ * all the necessary fields exist.
+ * This is not relevant for unit tests, since unit tests reset the database content anyway.
+ *
+ * @param IDatabase $db
+ * @return bool
+ */
+ protected function hasPreMcrFields( IDatabase $db ) {
+ return $db->fieldExists( 'revision', 'rev_content_model', __METHOD__ );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the post-migration
+ * MCR database schema.
+ */
+trait McrSchemaOverride {
+
+ use PatchFileLocation;
+ use McrSchemaDetection;
+
+ /**
+ * @return int
+ */
+ protected function getMcrMigrationStage() {
+ return MIGRATION_NEW;
+ }
+
+ /**
+ * @return string[]
+ */
+ protected function getMcrTablesToReset() {
+ return [
+ 'content',
+ 'content_models',
+ 'slots',
+ 'slot_roles',
+ ];
+ }
+
+ protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+ $overrides = [
+ 'scripts' => [],
+ 'drop' => [],
+ 'create' => [],
+ 'alter' => [],
+ ];
+
+ if ( !$this->hasMcrTables( $db ) ) {
+ $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots.sql' );
+ }
+
+ if ( !$this->hasPreMcrFields( $db ) ) {
+ $overrides['alter'][] = 'revision';
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'drop-pre-mcr-fields', __DIR__ );
+ }
+
+ return $overrides;
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use Revision;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+ use McrWriteBothSchemaOverride;
+
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ $row = parent::revisionToRow( $rev, $options );
+
+ $row->rev_text_id = (string)$rev->getTextId();
+ $row->rev_content_format = (string)$rev->getContentFormat();
+ $row->rev_content_model = (string)$rev->getContentModel();
+
+ return $row;
+ }
+
+ protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+ // New schema is being written
+ $this->assertSelect(
+ 'slots',
+ [ 'count(*)' ],
+ [ 'slot_revision_id' => $rev->getId() ],
+ [ [ '1' ] ]
+ );
+
+ $this->assertSelect(
+ 'content',
+ [ 'count(*)' ],
+ [ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
+ [ [ '1' ] ]
+ );
+
+ // Legacy schema is still being written
+ $this->assertSelect(
+ [ 'revision', 'text' ],
+ [ 'count(*)' ],
+ [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
+ [ [ 1 ] ],
+ [],
+ [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+ );
+
+ parent::assertRevisionExistsInDatabase( $rev );
+ }
+
+ /**
+ * @param SlotRecord $a
+ * @param SlotRecord $b
+ */
+ protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+ parent::assertSameSlotContent( $a, $b );
+
+ // Assert that the same content ID has been used
+ if ( $a->hasContentId() && $b->hasContentId() ) {
+ $this->assertSame( $a->getContentId(), $b->getContentId() );
+ }
+ }
+
+ public function provideInsertRevisionOn_failures() {
+ foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+ yield $case;
+ }
+
+ yield 'slot that is not main slot' => [
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'lalala' => new WikitextContent( 'Duck' ),
+ ],
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ new InvalidArgumentException( 'Only the main slot is supported' )
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, with page & id' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'text_id' => 2,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'content_format' => 'text/x-wiki',
+ 'content_model' => 'wikitext',
+ ]
+ ];
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+ * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+ */
+ public function testInsertRevisionFromArchiveRow_unmigratedArchiveRow() {
+ // The main purpose of this test is to assert that after reading an archive
+ // row using the old schema it can be inserted into the revision table,
+ // and a slot row is created based on slot emulated from the old-style archive row,
+ // when none such slot row exists yet.
+
+ $title = $this->getTestPage()->getTitle();
+
+ $this->db->insert(
+ 'text',
+ [ 'old_text' => 'Just a test', 'old_flags' => 'utf-8' ],
+ __METHOD__
+ );
+
+ $textId = $this->db->insertId();
+
+ $row = (object)[
+ 'ar_minor_edit' => '0',
+ 'ar_user' => '0',
+ 'ar_user_text' => '127.0.0.1',
+ 'ar_actor' => null,
+ 'ar_len' => '11',
+ 'ar_deleted' => '0',
+ 'ar_rev_id' => 112277,
+ 'ar_timestamp' => $this->db->timestamp( '20180101000000' ),
+ 'ar_sha1' => 'deadbeef',
+ 'ar_page_id' => $title->getArticleID(),
+ 'ar_comment_text' => 'just a test',
+ 'ar_comment_data' => null,
+ 'ar_comment_cid' => null,
+ 'ar_content_format' => null,
+ 'ar_content_model' => null,
+ 'ts_tags' => null,
+ 'ar_id' => 17,
+ 'ar_namespace' => $title->getNamespace(),
+ 'ar_title' => $title->getDBkey(),
+ 'ar_text_id' => $textId,
+ 'ar_parent_id' => 112211,
+ ];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $rev = $store->newRevisionFromArchiveRow( $row );
+
+ // re-insert archived revision
+ $return = $store->insertRevisionOn( $rev, $this->db );
+
+ // is the new revision correct?
+ $this->assertRevisionCompleteness( $return );
+ $this->assertRevisionRecordsEqual( $rev, $return );
+
+ // can we load it from the store?
+ $loaded = $store->getRevisionById( $return->getId() );
+ $this->assertNotNull( $loaded );
+ $this->assertRevisionCompleteness( $loaded );
+ $this->assertRevisionRecordsEqual( $return, $loaded );
+
+ // can we find it directly in the database?
+ $this->assertRevisionExistsInDatabase( $return );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the intermediate MCR database
+ * schema for use during schema migration.
+ */
+trait McrWriteBothSchemaOverride {
+
+ use PatchFileLocation;
+ use McrSchemaDetection;
+
+ /**
+ * @return int
+ */
+ protected function getMcrMigrationStage() {
+ return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD;
+ }
+
+ /**
+ * @return string[]
+ */
+ protected function getMcrTablesToReset() {
+ return [ 'content', 'content_models', 'slots', 'slot_roles' ];
+ }
+
+ /**
+ * @return array[]
+ */
+ protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+ $overrides = [
+ 'scripts' => [],
+ 'drop' => [],
+ 'create' => [],
+ 'alter' => [],
+ ];
+
+ if ( !$this->hasMcrTables( $db ) ) {
+ $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
+ }
+
+ if ( !$this->hasPreMcrFields( $db ) ) {
+ $overrides['alter'][] = 'revision';
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
+ }
+
+ return $overrides;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use TextContent;
+use Title;
+use User;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\MutableRevisionRecord
+ * @covers \MediaWiki\Revision\RevisionRecord
+ */
+class MutableRevisionRecordTest extends MediaWikiTestCase {
+
+ use RevisionRecordTests;
+
+ /**
+ * @param array $rowOverrides
+ *
+ * @return MutableRevisionRecord
+ */
+ protected function newRevision( array $rowOverrides = [] ) {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
+ $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+ $record = new MutableRevisionRecord( $title );
+
+ if ( isset( $rowOverrides['rev_deleted'] ) ) {
+ $record->setVisibility( $rowOverrides['rev_deleted'] );
+ }
+
+ if ( isset( $rowOverrides['rev_id'] ) ) {
+ $record->setId( $rowOverrides['rev_id'] );
+ }
+
+ if ( isset( $rowOverrides['rev_page'] ) ) {
+ $record->setPageId( $rowOverrides['rev_page'] );
+ }
+
+ $record->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+ $record->setComment( $comment );
+ $record->setUser( $user );
+ $record->setTimestamp( '20101010000000' );
+
+ return $record;
+ }
+
+ public function provideConstructor() {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ yield [
+ $title,
+ 'acmewiki'
+ ];
+ }
+
+ /**
+ * @dataProvider provideConstructor
+ *
+ * @param Title $title
+ * @param bool $wikiId
+ */
+ public function testConstructorAndGetters(
+ Title $title,
+ $wikiId = false
+ ) {
+ $rec = new MutableRevisionRecord( $title, $wikiId );
+
+ $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
+ $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
+ }
+
+ public function provideConstructorFailure() {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ yield 'not a wiki id' => [
+ $title,
+ null
+ ];
+ }
+
+ /**
+ * @dataProvider provideConstructorFailure
+ *
+ * @param Title $title
+ * @param bool $wikiId
+ */
+ public function testConstructorFailure(
+ Title $title,
+ $wikiId = false
+ ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+ new MutableRevisionRecord( $title, $wikiId );
+ }
+
+ public function testSetGetId() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertNull( $record->getId() );
+ $record->setId( 888 );
+ $this->assertSame( 888, $record->getId() );
+ }
+
+ public function testSetGetUser() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $user = $this->getTestSysop()->getUser();
+ $this->assertNull( $record->getUser() );
+ $record->setUser( $user );
+ $this->assertSame( $user, $record->getUser() );
+ }
+
+ public function testSetGetPageId() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertSame( 0, $record->getPageId() );
+ $record->setPageId( 999 );
+ $this->assertSame( 999, $record->getPageId() );
+ }
+
+ public function testSetGetParentId() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertNull( $record->getParentId() );
+ $record->setParentId( 100 );
+ $this->assertSame( 100, $record->getParentId() );
+ }
+
+ public function testGetMainContentWhenEmpty() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->setExpectedException( RevisionAccessException::class );
+ $this->assertNull( $record->getContent( SlotRecord::MAIN ) );
+ }
+
+ public function testSetGetMainContent() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $content = new WikitextContent( 'Badger' );
+ $record->setContent( SlotRecord::MAIN, $content );
+ $this->assertSame( $content, $record->getContent( SlotRecord::MAIN ) );
+ }
+
+ public function testGetSlotWhenEmpty() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertFalse( $record->hasSlot( SlotRecord::MAIN ) );
+
+ $this->setExpectedException( RevisionAccessException::class );
+ $record->getSlot( SlotRecord::MAIN );
+ }
+
+ public function testSetGetSlot() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $slot = SlotRecord::newUnsaved(
+ SlotRecord::MAIN,
+ new WikitextContent( 'x' )
+ );
+ $record->setSlot( $slot );
+ $this->assertTrue( $record->hasSlot( SlotRecord::MAIN ) );
+ $this->assertSame( $slot, $record->getSlot( SlotRecord::MAIN ) );
+ }
+
+ public function testSetGetMinor() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertFalse( $record->isMinor() );
+ $record->setMinorEdit( true );
+ $this->assertSame( true, $record->isMinor() );
+ }
+
+ public function testSetGetTimestamp() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertNull( $record->getTimestamp() );
+ $record->setTimestamp( '20180101010101' );
+ $this->assertSame( '20180101010101', $record->getTimestamp() );
+ }
+
+ public function testSetGetVisibility() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertSame( 0, $record->getVisibility() );
+ $record->setVisibility( RevisionRecord::DELETED_USER );
+ $this->assertSame( RevisionRecord::DELETED_USER, $record->getVisibility() );
+ }
+
+ public function testSetGetSha1() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertSame( 'phoiac9h4m842xq45sp7s6u21eteeq1', $record->getSha1() );
+ $record->setSha1( 'someHash' );
+ $this->assertSame( 'someHash', $record->getSha1() );
+ }
+
+ public function testGetSlots() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertInstanceOf( MutableRevisionSlots::class, $record->getSlots() );
+ }
+
+ public function testSetGetSize() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $this->assertSame( 0, $record->getSize() );
+ $record->setSize( 775 );
+ $this->assertSame( 775, $record->getSize() );
+ }
+
+ public function testSetGetComment() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $comment = new CommentStoreComment( 1, 'foo' );
+ $this->assertNull( $record->getComment() );
+ $record->setComment( $comment );
+ $this->assertSame( $comment, $record->getComment() );
+ }
+
+ public function testSimpleGetOriginalAndInheritedSlots() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $mainSlot = new SlotRecord(
+ (object)[
+ 'slot_id' => 1,
+ 'slot_revision_id' => null, // unsaved
+ 'slot_content_id' => 1,
+ 'content_address' => null, // touched
+ 'model_name' => 'x',
+ 'role_name' => 'main',
+ 'slot_origin' => null // touched
+ ],
+ new WikitextContent( 'main' )
+ );
+ $auxSlot = new SlotRecord(
+ (object)[
+ 'slot_id' => 2,
+ 'slot_revision_id' => null, // unsaved
+ 'slot_content_id' => 1,
+ 'content_address' => 'foo', // inherited
+ 'model_name' => 'x',
+ 'role_name' => 'aux',
+ 'slot_origin' => 1 // inherited
+ ],
+ new WikitextContent( 'aux' )
+ );
+
+ $record->setSlot( $mainSlot );
+ $record->setSlot( $auxSlot );
+
+ $this->assertSame( [ 'main' ], $record->getOriginalSlots()->getSlotRoles() );
+ $this->assertSame( $mainSlot, $record->getOriginalSlots()->getSlot( SlotRecord::MAIN ) );
+
+ $this->assertSame( [ 'aux' ], $record->getInheritedSlots()->getSlotRoles() );
+ $this->assertSame( $auxSlot, $record->getInheritedSlots()->getSlot( 'aux' ) );
+ }
+
+ public function testSimpleremoveSlot() {
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+
+ $a = new WikitextContent( 'a' );
+ $b = new WikitextContent( 'b' );
+
+ $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
+ $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
+
+ $record->removeSlot( 'b' );
+
+ $this->assertTrue( $record->hasSlot( 'a' ) );
+ $this->assertFalse( $record->hasSlot( 'b' ) );
+ }
+
+ public function testApplyUpdate() {
+ $update = new RevisionSlotsUpdate();
+
+ $a = new WikitextContent( 'a' );
+ $b = new WikitextContent( 'b' );
+ $c = new WikitextContent( 'c' );
+ $x = new WikitextContent( 'x' );
+
+ $update->modifyContent( 'b', $x );
+ $update->modifyContent( 'c', $x );
+ $update->removeSlot( 'c' );
+ $update->removeSlot( 'd' );
+
+ $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+ $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
+ $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
+ $record->inheritSlot( SlotRecord::newSaved( 7, 5, 'c', SlotRecord::newUnsaved( 'c', $c ) ) );
+
+ $record->applyUpdate( $update );
+
+ $this->assertEquals( [ 'b' ], array_keys( $record->getOriginalSlots()->getSlots() ) );
+ $this->assertEquals( $a, $record->getSlot( 'a' )->getContent() );
+ $this->assertEquals( $x, $record->getSlot( 'b' )->getContent() );
+ $this->assertFalse( $record->hasSlot( 'c' ) );
+ }
+
+ public function provideNotReadyForInsertion() {
+ /** @var Title $title */
+ $title = $this->getMock( Title::class );
+
+ /** @var User $user */
+ $user = $this->getMock( User::class );
+
+ /** @var CommentStoreComment $comment */
+ $comment = $this->getMockBuilder( CommentStoreComment::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $content = new TextContent( 'Test' );
+
+ $rev = new MutableRevisionRecord( $title );
+ yield 'empty' => [ $rev ];
+
+ $rev = new MutableRevisionRecord( $title );
+ $rev->setContent( SlotRecord::MAIN, $content );
+ $rev->setUser( $user );
+ $rev->setComment( $comment );
+ yield 'no timestamp' => [ $rev ];
+
+ $rev = new MutableRevisionRecord( $title );
+ $rev->setUser( $user );
+ $rev->setComment( $comment );
+ $rev->setTimestamp( '20101010000000' );
+ yield 'no content' => [ $rev ];
+
+ $rev = new MutableRevisionRecord( $title );
+ $rev->setContent( SlotRecord::MAIN, $content );
+ $rev->setComment( $comment );
+ $rev->setTimestamp( '20101010000000' );
+ yield 'no user' => [ $rev ];
+
+ $rev = new MutableRevisionRecord( $title );
+ $rev->setUser( $user );
+ $rev->setContent( SlotRecord::MAIN, $content );
+ $rev->setTimestamp( '20101010000000' );
+ yield 'no comment' => [ $rev ];
+ }
+
+ /**
+ * @dataProvider provideNotReadyForInsertion
+ */
+ public function testNotReadyForInsertion( $rev ) {
+ $this->assertFalse( $rev->isReadyForInsertion() );
+ }
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use Content;
+use InvalidArgumentException;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\SlotRecord;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\MutableRevisionSlots
+ */
+class MutableRevisionSlotsTest extends RevisionSlotsTest {
+
+ /**
+ * @param SlotRecord[] $slots
+ * @return RevisionSlots
+ */
+ protected function newRevisionSlots( $slots = [] ) {
+ return new MutableRevisionSlots( $slots );
+ }
+
+ public function provideConstructorFailue() {
+ yield 'array or the wrong thing' => [
+ [ 1, 2, 3 ]
+ ];
+ }
+
+ /**
+ * @dataProvider provideConstructorFailue
+ * @param $slots
+ *
+ * @covers \MediaWiki\Revision\RevisionSlots::__construct
+ * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal
+ */
+ public function testConstructorFailue( $slots ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+
+ new MutableRevisionSlots( $slots );
+ }
+
+ public function testSetMultipleSlots() {
+ $slots = new MutableRevisionSlots();
+
+ $this->assertSame( [], $slots->getSlots() );
+
+ $slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) );
+ $slots->setSlot( $slotA );
+ $this->assertTrue( $slots->hasSlot( 'some' ) );
+ $this->assertSame( $slotA, $slots->getSlot( 'some' ) );
+ $this->assertSame( [ 'some' => $slotA ], $slots->getSlots() );
+
+ $slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) );
+ $slots->setSlot( $slotB );
+ $this->assertTrue( $slots->hasSlot( 'other' ) );
+ $this->assertSame( $slotB, $slots->getSlot( 'other' ) );
+ $this->assertSame( [ 'some' => $slotA, 'other' => $slotB ], $slots->getSlots() );
+ }
+
+ public function testSetExistingSlotOverwritesSlot() {
+ $slots = new MutableRevisionSlots();
+
+ $this->assertSame( [], $slots->getSlots() );
+
+ $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $slots->setSlot( $slotA );
+ $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
+ $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
+
+ $slotB = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'B' ) );
+ $slots->setSlot( $slotB );
+ $this->assertSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
+ $this->assertSame( [ 'main' => $slotB ], $slots->getSlots() );
+ }
+
+ /**
+ * @param string $role
+ * @param Content $content
+ * @return SlotRecord
+ */
+ private function newSavedSlot( $role, Content $content ) {
+ return SlotRecord::newSaved( 7, 7, 'xyz', SlotRecord::newUnsaved( $role, $content ) );
+ }
+
+ public function testInheritSlotOverwritesSlot() {
+ $slots = new MutableRevisionSlots();
+ $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $slots->setSlot( $slotA );
+ $slotB = $this->newSavedSlot( SlotRecord::MAIN, new WikitextContent( 'B' ) );
+ $slotC = $this->newSavedSlot( 'foo', new WikitextContent( 'C' ) );
+ $slots->inheritSlot( $slotB );
+ $slots->inheritSlot( $slotC );
+ $this->assertSame( [ 'main', 'foo' ], $slots->getSlotRoles() );
+ $this->assertNotSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
+ $this->assertNotSame( $slotC, $slots->getSlot( 'foo' ) );
+ $this->assertTrue( $slots->getSlot( SlotRecord::MAIN )->isInherited() );
+ $this->assertTrue( $slots->getSlot( 'foo' )->isInherited() );
+ $this->assertSame( $slotB->getContent(), $slots->getSlot( SlotRecord::MAIN )->getContent() );
+ $this->assertSame( $slotC->getContent(), $slots->getSlot( 'foo' )->getContent() );
+ }
+
+ public function testSetContentOfExistingSlotOverwritesContent() {
+ $slots = new MutableRevisionSlots();
+
+ $this->assertSame( [], $slots->getSlots() );
+
+ $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $slots->setSlot( $slotA );
+ $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
+ $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
+
+ $newContent = new WikitextContent( 'B' );
+ $slots->setContent( SlotRecord::MAIN, $newContent );
+ $this->assertSame( $newContent, $slots->getContent( SlotRecord::MAIN ) );
+ }
+
+ public function testRemoveExistingSlot() {
+ $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $slots = new MutableRevisionSlots( [ $slotA ] );
+
+ $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
+
+ $slots->removeSlot( SlotRecord::MAIN );
+ $this->assertSame( [], $slots->getSlots() );
+ $this->setExpectedException( RevisionAccessException::class );
+ $slots->getSlot( SlotRecord::MAIN );
+ }
+
+ public function testNewFromParentRevisionSlots() {
+ /** @var SlotRecord[] $parentSlots */
+ $parentSlots = [
+ 'some' => $this->newSavedSlot( 'some', new WikitextContent( 'X' ) ),
+ 'other' => $this->newSavedSlot( 'other', new WikitextContent( 'Y' ) ),
+ ];
+ $slots = MutableRevisionSlots::newFromParentRevisionSlots( $parentSlots );
+ $this->assertSame( [ 'some', 'other' ], $slots->getSlotRoles() );
+ $this->assertNotSame( $parentSlots['some'], $slots->getSlot( 'some' ) );
+ $this->assertNotSame( $parentSlots['other'], $slots->getSlot( 'other' ) );
+ $this->assertTrue( $slots->getSlot( 'some' )->isInherited() );
+ $this->assertTrue( $slots->getSlot( 'other' )->isInherited() );
+ $this->assertSame( $parentSlots['some']->getContent(), $slots->getContent( 'some' ) );
+ $this->assertSame( $parentSlots['other']->getContent(), $slots->getContent( 'other' ) );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Revision;
+
+/**
+ * Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+ use PreMcrSchemaOverride;
+
+ protected function getContentHandlerUseDB() {
+ return false;
+ }
+
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ $row = parent::revisionToRow( $rev, $options );
+
+ $row->rev_text_id = (string)$rev->getTextId();
+
+ return $row;
+ }
+
+ public function provideGetArchiveQueryInfo() {
+ yield [
+ [
+ 'tables' => [ 'archive' ],
+ 'fields' => array_merge(
+ $this->getDefaultArchiveFields(),
+ [
+ 'ar_comment_text' => 'ar_comment',
+ 'ar_comment_data' => 'NULL',
+ 'ar_comment_cid' => 'NULL',
+ 'ar_user_text' => 'ar_user_text',
+ 'ar_user' => 'ar_user',
+ 'ar_actor' => 'NULL',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ }
+
+ public function provideGetQueryInfo() {
+ yield [
+ [],
+ [
+ 'tables' => [ 'revision' ],
+ 'fields' => array_merge(
+ $this->getDefaultQueryFields(),
+ $this->getCommentQueryFields(),
+ $this->getActorQueryFields()
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield [
+ [ 'page' ],
+ [
+ 'tables' => [ 'revision', 'page' ],
+ 'fields' => array_merge(
+ $this->getDefaultQueryFields(),
+ $this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
+ [
+ 'page_namespace',
+ 'page_title',
+ 'page_id',
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
+ ]
+ ),
+ 'joins' => [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ ],
+ ]
+ ];
+ yield [
+ [ 'user' ],
+ [
+ 'tables' => [ 'revision', 'user' ],
+ 'fields' => array_merge(
+ $this->getDefaultQueryFields(),
+ $this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
+ [
+ 'user_name',
+ ]
+ ),
+ 'joins' => [
+ 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+ ],
+ ]
+ ];
+ yield [
+ [ 'text' ],
+ [
+ 'tables' => [ 'revision', 'text' ],
+ 'fields' => array_merge(
+ $this->getDefaultQueryFields(),
+ $this->getCommentQueryFields(),
+ $this->getActorQueryFields(),
+ [
+ 'old_text',
+ 'old_flags',
+ ]
+ ),
+ 'joins' => [
+ 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
+ ],
+ ]
+ ];
+ }
+
+ public function provideGetSlotsQueryInfo() {
+ $db = wfGetDB( DB_REPLICA );
+
+ yield [
+ [],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield [
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+ 'content_size' => 'slots.rev_len',
+ 'content_sha1' => 'slots.rev_sha1',
+ 'content_address' =>
+ $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+ 'model_name' => 'NULL',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, with page & id' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'text_id' => 2,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ ]
+ ];
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionRecord;
+use Revision;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the pre-MCR DB schema.
+ *
+ * @covers \MediaWiki\Revision\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+ use PreMcrSchemaOverride;
+
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ $row = parent::revisionToRow( $rev, $options );
+
+ $row->rev_text_id = (string)$rev->getTextId();
+ $row->rev_content_format = (string)$rev->getContentFormat();
+ $row->rev_content_model = (string)$rev->getContentModel();
+
+ return $row;
+ }
+
+ protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+ // Legacy schema is still being written
+ $this->assertSelect(
+ [ 'revision', 'text' ],
+ [ 'count(*)' ],
+ [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
+ [ [ 1 ] ],
+ [],
+ [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
+ );
+
+ parent::assertRevisionExistsInDatabase( $rev );
+ }
+
+ public function provideInsertRevisionOn_failures() {
+ foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+ yield $case;
+ }
+
+ yield 'slot that is not main slot' => [
+ [
+ 'content' => [
+ 'main' => new WikitextContent( 'Chicken' ),
+ 'lalala' => new WikitextContent( 'Duck' ),
+ ],
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ new InvalidArgumentException( 'Only the main slot is supported' )
+ ];
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+ yield $case;
+ }
+
+ yield 'Basic array, with page & id' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'text_id' => 2,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'content_format' => 'text/x-wiki',
+ 'content_model' => 'wikitext',
+ ]
+ ];
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the pre-MCR database schema.
+ */
+trait PreMcrSchemaOverride {
+
+ use PatchFileLocation;
+ use McrSchemaDetection;
+
+ /**
+ * @return int
+ */
+ protected function getMcrMigrationStage() {
+ return MIGRATION_OLD;
+ }
+
+ /**
+ * @return string[]
+ */
+ protected function getMcrTablesToReset() {
+ return [];
+ }
+
+ /**
+ * @return array[]
+ */
+ protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+ $overrides = [
+ 'scripts' => [],
+ 'drop' => [],
+ 'create' => [],
+ 'alter' => [],
+ ];
+
+ if ( $this->hasMcrTables( $db ) ) {
+ $overrides['drop'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/drop-mcr-tables', __DIR__ );
+ }
+
+ if ( !$this->hasPreMcrFields( $db ) ) {
+ $overrides['alter'][] = 'revision';
+ $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/create-pre-mcr-fields', __DIR__ );
+ }
+
+ return $overrides;
+ }
+
+}
use Content;
use Language;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\MutableRevisionSlots;
use MediaWiki\Revision\RenderedRevision;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionArchiveRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SuppressedDataException;
-use MediaWiki\User\UserIdentityValue;
+use MediaWiki\Revision\RevisionArchiveRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
use MediaWikiTestCase;
+use MediaWiki\User\UserIdentityValue;
use ParserOptions;
use ParserOutput;
use PHPUnit\Framework\MockObject\MockObject;
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionArchiveRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use TextContent;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\RevisionArchiveRecord
+ * @covers \MediaWiki\Revision\RevisionRecord
+ */
+class RevisionArchiveRecordTest extends MediaWikiTestCase {
+
+ use RevisionRecordTests;
+
+ /**
+ * @param array $rowOverrides
+ *
+ * @return RevisionArchiveRecord
+ */
+ protected function newRevision( array $rowOverrides = [] ) {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
+ $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+ $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+ $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+ $slots = new RevisionSlots( [ $main, $aux ] );
+
+ $row = [
+ 'ar_id' => '5',
+ 'ar_rev_id' => '7',
+ 'ar_page_id' => strval( $title->getArticleID() ),
+ 'ar_timestamp' => '20200101000000',
+ 'ar_deleted' => 0,
+ 'ar_minor_edit' => 0,
+ 'ar_parent_id' => '5',
+ 'ar_len' => $slots->computeSize(),
+ 'ar_sha1' => $slots->computeSha1(),
+ ];
+
+ foreach ( $rowOverrides as $field => $value ) {
+ $field = preg_replace( '/^rev_/', 'ar_', $field );
+ $row[$field] = $value;
+ }
+
+ return new RevisionArchiveRecord( $title, $user, $comment, (object)$row, $slots );
+ }
+
+ public function provideConstructor() {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
+ $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+ $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+ $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+ $slots = new RevisionSlots( [ $main, $aux ] );
+
+ $protoRow = [
+ 'ar_id' => '5',
+ 'ar_rev_id' => '7',
+ 'ar_page_id' => strval( $title->getArticleID() ),
+ 'ar_timestamp' => '20200101000000',
+ 'ar_deleted' => 0,
+ 'ar_minor_edit' => 0,
+ 'ar_parent_id' => '5',
+ 'ar_len' => $slots->computeSize(),
+ 'ar_sha1' => $slots->computeSha1(),
+ ];
+
+ $row = $protoRow;
+ yield 'all info' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots,
+ 'acmewiki'
+ ];
+
+ $row = $protoRow;
+ $row['ar_minor_edit'] = '1';
+ $row['ar_deleted'] = strval( RevisionRecord::DELETED_USER );
+
+ yield 'minor deleted' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ unset( $row['ar_parent'] );
+
+ yield 'no parent' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ $row['ar_len'] = null;
+ $row['ar_sha1'] = '';
+
+ yield 'ar_len is null, ar_sha1 is ""' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ yield 'no length, no hash' => [
+ Title::newFromText( 'DummyDoesNotExist' ),
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+ }
+
+ /**
+ * @dataProvider provideConstructor
+ *
+ * @param Title $title
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row
+ * @param RevisionSlots $slots
+ * @param bool $wikiId
+ */
+ public function testConstructorAndGetters(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ $rec = new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
+
+ $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
+ $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
+ $this->assertSame( $comment, $rec->getComment(), 'getComment' );
+
+ $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
+ $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
+
+ $this->assertSame( (int)$row->ar_id, $rec->getArchiveId(), 'getArchiveId' );
+ $this->assertSame( (int)$row->ar_rev_id, $rec->getId(), 'getId' );
+ $this->assertSame( (int)$row->ar_page_id, $rec->getPageId(), 'getId' );
+ $this->assertSame( $row->ar_timestamp, $rec->getTimestamp(), 'getTimestamp' );
+ $this->assertSame( (int)$row->ar_deleted, $rec->getVisibility(), 'getVisibility' );
+ $this->assertSame( (bool)$row->ar_minor_edit, $rec->isMinor(), 'getIsMinor' );
+
+ if ( isset( $row->ar_parent_id ) ) {
+ $this->assertSame( (int)$row->ar_parent_id, $rec->getParentId(), 'getParentId' );
+ } else {
+ $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
+ }
+
+ if ( isset( $row->ar_len ) ) {
+ $this->assertSame( (int)$row->ar_len, $rec->getSize(), 'getSize' );
+ } else {
+ $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
+ }
+
+ if ( !empty( $row->ar_sha1 ) ) {
+ $this->assertSame( $row->ar_sha1, $rec->getSha1(), 'getSha1' );
+ } else {
+ $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
+ }
+ }
+
+ public function provideConstructorFailure() {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
+
+ $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+ $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+ $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+ $slots = new RevisionSlots( [ $main, $aux ] );
+
+ $protoRow = [
+ 'ar_id' => '5',
+ 'ar_rev_id' => '7',
+ 'ar_page_id' => strval( $title->getArticleID() ),
+ 'ar_timestamp' => '20200101000000',
+ 'ar_deleted' => 0,
+ 'ar_minor_edit' => 0,
+ 'ar_parent_id' => '5',
+ 'ar_len' => $slots->computeSize(),
+ 'ar_sha1' => $slots->computeSha1(),
+ ];
+
+ yield 'not a row' => [
+ $title,
+ $user,
+ $comment,
+ 'not a row',
+ $slots,
+ 'acmewiki'
+ ];
+
+ $row = $protoRow;
+ $row['ar_timestamp'] = 'kittens';
+
+ yield 'bad timestamp' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+
+ yield 'bad wiki' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots,
+ 12345
+ ];
+
+ // NOTE: $title->getArticleID does *not* have to match ar_page_id in all cases!
+ }
+
+ /**
+ * @dataProvider provideConstructorFailure
+ *
+ * @param Title $title
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row
+ * @param RevisionSlots $slots
+ * @param bool $wikiId
+ */
+ public function testConstructorFailure(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+ new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
+ }
+
+}
--- /dev/null
+<?php
+namespace MediaWiki\Tests\Revision;
+
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
+use MediaWikiTestCase;
+use Revision;
+
+/**
+ * Tests RevisionStore against the post-migration MCR DB schema.
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ */
+class RevisionQueryInfoTest extends MediaWikiTestCase {
+
+ protected function getRevisionQueryFields( $returnTextIdField = true ) {
+ $fields = [
+ 'rev_id',
+ 'rev_page',
+ 'rev_timestamp',
+ 'rev_minor_edit',
+ 'rev_deleted',
+ 'rev_len',
+ 'rev_parent_id',
+ 'rev_sha1',
+ ];
+ if ( $returnTextIdField ) {
+ $fields[] = 'rev_text_id';
+ }
+ return $fields;
+ }
+
+ protected function getArchiveQueryFields( $returnTextFields = true ) {
+ $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',
+ ];
+ if ( $returnTextFields ) {
+ $fields[] = 'ar_text_id';
+ }
+ return $fields;
+ }
+
+ protected function getOldCommentQueryFields( $prefix ) {
+ return [
+ "{$prefix}_comment_text" => "{$prefix}_comment",
+ "{$prefix}_comment_data" => 'NULL',
+ "{$prefix}_comment_cid" => 'NULL',
+ ];
+ }
+
+ protected function getCompatCommentQueryFields( $prefix ) {
+ return [
+ "{$prefix}_comment_text"
+ => "COALESCE( comment_{$prefix}_comment.comment_text, {$prefix}_comment )",
+ "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
+ "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
+ ];
+ }
+
+ protected function getNewCommentQueryFields( $prefix ) {
+ return [
+ "{$prefix}_comment_text" => "comment_{$prefix}_comment.comment_text",
+ "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
+ "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
+ ];
+ }
+
+ protected function getOldActorQueryFields( $prefix ) {
+ return [
+ "{$prefix}_user" => "{$prefix}_user",
+ "{$prefix}_user_text" => "{$prefix}_user_text",
+ "{$prefix}_actor" => 'NULL',
+ ];
+ }
+
+ protected function getNewActorQueryFields( $prefix, $tmp = false ) {
+ return [
+ "{$prefix}_user" => "actor_{$prefix}_user.actor_user",
+ "{$prefix}_user_text" => "actor_{$prefix}_user.actor_name",
+ "{$prefix}_actor" => $tmp ?: "{$prefix}_actor",
+ ];
+ }
+
+ protected function getCompatActorQueryFields( $prefix, $tmp = false ) {
+ return [
+ "{$prefix}_user" => "COALESCE( actor_{$prefix}_user.actor_user, {$prefix}_user )",
+ "{$prefix}_user_text" => "COALESCE( actor_{$prefix}_user.actor_name, {$prefix}_user_text )",
+ "{$prefix}_actor" => $tmp ?: "{$prefix}_actor",
+ ];
+ }
+
+ protected function getCompatActorJoins( $prefix ) {
+ return [
+ "temp_{$prefix}_user" => [
+ "LEFT JOIN",
+ "temp_{$prefix}_user.revactor_{$prefix} = {$prefix}_id",
+ ],
+ "actor_{$prefix}_user" => [
+ "LEFT JOIN",
+ "actor_{$prefix}_user.actor_id = temp_{$prefix}_user.revactor_actor",
+ ],
+ ];
+ }
+
+ protected function getCompatCommentJoins( $prefix ) {
+ return [
+ "temp_{$prefix}_comment" => [
+ "LEFT JOIN",
+ "temp_{$prefix}_comment.revcomment_{$prefix} = {$prefix}_id",
+ ],
+ "comment_{$prefix}_comment" => [
+ "LEFT JOIN",
+ "comment_{$prefix}_comment.comment_id = temp_{$prefix}_comment.revcomment_comment_id",
+ ],
+ ];
+ }
+
+ protected function getTextQueryFields() {
+ return [
+ 'old_text',
+ 'old_flags',
+ ];
+ }
+
+ protected function getPageQueryFields() {
+ return [
+ 'page_namespace',
+ 'page_title',
+ 'page_id',
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
+ ];
+ }
+
+ protected function getUserQueryFields() {
+ return [
+ 'user_name',
+ ];
+ }
+
+ protected function getContentHandlerQueryFields( $prefix ) {
+ return [
+ "{$prefix}_content_format",
+ "{$prefix}_content_model",
+ ];
+ }
+
+ public function provideArchiveQueryInfo() {
+ yield 'MCR, comment, actor' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_NEW,
+ ],
+ [
+ 'tables' => [
+ 'archive',
+ 'actor_ar_user' => 'actor',
+ 'comment_ar_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getArchiveQueryFields( false ),
+ $this->getNewActorQueryFields( 'ar' ),
+ $this->getNewCommentQueryFields( 'ar' )
+ ),
+ 'joins' => [
+ 'comment_ar_comment'
+ => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+ 'actor_ar_user' => [ 'JOIN', 'actor_ar_user.actor_id = ar_actor' ],
+ ],
+ ]
+ ];
+ yield 'read-new MCR, comment, actor' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+ ],
+ [
+ 'tables' => [
+ 'archive',
+ 'actor_ar_user' => 'actor',
+ 'comment_ar_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getArchiveQueryFields( false ),
+ $this->getCompatActorQueryFields( 'ar' ),
+ $this->getCompatCommentQueryFields( 'ar' )
+ ),
+ 'joins' => [
+ 'comment_ar_comment'
+ => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+ 'actor_ar_user' => [ 'LEFT JOIN', 'actor_ar_user.actor_id = ar_actor' ],
+ ],
+ ]
+ ];
+ yield 'MCR write-both/read-old' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ ],
+ [
+ 'tables' => [
+ 'archive',
+ 'actor_ar_user' => 'actor',
+ 'comment_ar_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getArchiveQueryFields( true ),
+ $this->getContentHandlerQueryFields( 'ar' ),
+ $this->getCompatActorQueryFields( 'ar' ),
+ $this->getCompatCommentQueryFields( 'ar' )
+ ),
+ 'joins' => [
+ 'comment_ar_comment'
+ => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+ 'actor_ar_user' => [ 'LEFT JOIN', 'actor_ar_user.actor_id = ar_actor' ],
+ ],
+ ]
+ ];
+ yield 'pre-MCR, no model' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [
+ 'tables' => [
+ 'archive',
+ ],
+ 'fields' => array_merge(
+ $this->getArchiveQueryFields( true ),
+ $this->getOldActorQueryFields( 'ar' ),
+ $this->getOldCommentQueryFields( 'ar' )
+ ),
+ 'joins' => [],
+ ]
+ ];
+ }
+
+ public function provideQueryInfo() {
+ // TODO: more option variations
+ yield 'MCR, page, user, comment, actor' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_NEW,
+ ],
+ [ 'page', 'user' ],
+ [
+ 'tables' => [
+ 'revision',
+ 'page',
+ 'user',
+ 'temp_rev_user' => 'revision_actor_temp',
+ 'temp_rev_comment' => 'revision_comment_temp',
+ 'actor_rev_user' => 'actor',
+ 'comment_rev_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( false ),
+ $this->getPageQueryFields(),
+ $this->getUserQueryFields(),
+ $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+ $this->getNewCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'user' => [
+ 'LEFT JOIN',
+ [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ],
+ ],
+ 'comment_rev_comment' => [
+ 'JOIN',
+ 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
+ ],
+ 'actor_rev_user' => [
+ 'JOIN',
+ 'actor_rev_user.actor_id = temp_rev_user.revactor_actor',
+ ],
+ 'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
+ 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+ ],
+ ]
+ ];
+ yield 'MCR read-new, page, user, comment, actor' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+ ],
+ [ 'page', 'user' ],
+ [
+ 'tables' => [
+ 'revision',
+ 'page',
+ 'user',
+ 'temp_rev_user' => 'revision_actor_temp',
+ 'temp_rev_comment' => 'revision_comment_temp',
+ 'actor_rev_user' => 'actor',
+ 'comment_rev_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( false ),
+ $this->getPageQueryFields(),
+ $this->getUserQueryFields(),
+ $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+ $this->getCompatCommentQueryFields( 'rev' )
+ ),
+ 'joins' => array_merge(
+ [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'user' => [
+ 'LEFT JOIN',
+ [
+ 'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
+ 'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
+ ]
+ ],
+ ],
+ $this->getCompatActorJoins( 'rev' ),
+ $this->getCompatCommentJoins( 'rev' )
+ ),
+ ]
+ ];
+ yield 'MCR read-new' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
+ ],
+ [ 'page', 'user' ],
+ [
+ 'tables' => [
+ 'revision',
+ 'page',
+ 'user',
+ 'temp_rev_user' => 'revision_actor_temp',
+ 'temp_rev_comment' => 'revision_comment_temp',
+ 'actor_rev_user' => 'actor',
+ 'comment_rev_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( false ),
+ $this->getPageQueryFields(),
+ $this->getUserQueryFields(),
+ $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+ $this->getCompatCommentQueryFields( 'rev' )
+ ),
+ 'joins' => array_merge(
+ [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'user' => [
+ 'LEFT JOIN',
+ [
+ 'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
+ 'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
+ ]
+ ],
+ ],
+ $this->getCompatActorJoins( 'rev' ),
+ $this->getCompatCommentJoins( 'rev' )
+ ),
+ ]
+ ];
+ yield 'MCR write-both/read-old' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ ],
+ [],
+ [
+ 'tables' => [
+ 'revision',
+ 'temp_rev_user' => 'revision_actor_temp',
+ 'temp_rev_comment' => 'revision_comment_temp',
+ 'actor_rev_user' => 'actor',
+ 'comment_rev_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getContentHandlerQueryFields( 'rev' ),
+ $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+ $this->getCompatCommentQueryFields( 'rev' )
+ ),
+ 'joins' => array_merge(
+ $this->getCompatActorJoins( 'rev' ),
+ $this->getCompatCommentJoins( 'rev' )
+ ),
+ ]
+ ];
+ yield 'MCR write-both/read-old, page, user' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ ],
+ [ 'page', 'user' ],
+ [
+ 'tables' => [
+ 'revision',
+ 'page',
+ 'user',
+ 'temp_rev_user' => 'revision_actor_temp',
+ 'temp_rev_comment' => 'revision_comment_temp',
+ 'actor_rev_user' => 'actor',
+ 'comment_rev_comment' => 'comment',
+ ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getContentHandlerQueryFields( 'rev' ),
+ $this->getUserQueryFields(),
+ $this->getPageQueryFields(),
+ $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
+ $this->getCompatCommentQueryFields( 'rev' )
+ ),
+ 'joins' => array_merge(
+ [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'user' => [
+ 'LEFT JOIN',
+ [
+ 'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
+ 'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
+ ]
+ ],
+ ],
+ $this->getCompatActorJoins( 'rev' ),
+ $this->getCompatCommentJoins( 'rev' )
+ ),
+ ]
+ ];
+ yield 'pre-MCR' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [],
+ [
+ 'tables' => [ 'revision' ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getContentHandlerQueryFields( 'rev' ),
+ $this->getOldActorQueryFields( 'rev' ),
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield 'pre-MCR, page, user' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [ 'page', 'user' ],
+ [
+ 'tables' => [ 'revision', 'page', 'user' ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getContentHandlerQueryFields( 'rev' ),
+ $this->getPageQueryFields(),
+ $this->getUserQueryFields(),
+ $this->getOldActorQueryFields( 'rev' ),
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+ ],
+ ]
+ ];
+ yield 'pre-MCR, no model' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [],
+ [
+ 'tables' => [ 'revision' ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getOldActorQueryFields( 'rev' ),
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [],
+ ],
+ ];
+ yield 'pre-MCR, no model, page' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [ 'page' ],
+ [
+ 'tables' => [ 'revision', 'page' ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getPageQueryFields(),
+ $this->getOldActorQueryFields( 'rev' ),
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [
+ 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ], ],
+ ],
+ ],
+ ];
+ yield 'pre-MCR, no model, user' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [ 'user' ],
+ [
+ 'tables' => [ 'revision', 'user' ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getUserQueryFields(),
+ $this->getOldActorQueryFields( 'rev' ),
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [
+ 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+ ],
+ ],
+ ];
+ yield 'pre-MCR, no model, text' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [ 'text' ],
+ [
+ 'tables' => [ 'revision', 'text' ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getTextQueryFields(),
+ $this->getOldActorQueryFields( 'rev' ),
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [
+ 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
+ ],
+ ],
+ ];
+ yield 'pre-MCR, no model, text, page, user' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ [ 'text', 'page', 'user' ],
+ [
+ 'tables' => [
+ 'revision', 'page', 'user', 'text'
+ ],
+ 'fields' => array_merge(
+ $this->getRevisionQueryFields( true ),
+ $this->getPageQueryFields(),
+ $this->getUserQueryFields(),
+ $this->getTextQueryFields(),
+ $this->getOldActorQueryFields( 'rev' ),
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ 'joins' => [
+ 'page' => [
+ 'INNER JOIN',
+ [ 'page_id = rev_page' ],
+ ],
+ 'user' => [
+ 'LEFT JOIN',
+ [
+ 'rev_user != 0',
+ 'user_id = rev_user',
+ ],
+ ],
+ 'text' => [
+ 'INNER JOIN',
+ [ 'rev_text_id=old_id' ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ public function provideSlotsQueryInfo() {
+ yield 'MCR, no options' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+ ],
+ [],
+ [
+ 'tables' => [
+ 'slots'
+ ],
+ 'fields' => [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'slot_role_id',
+ ],
+ 'joins' => [],
+ ]
+ ];
+ yield 'MCR, role option' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+ ],
+ [ 'role' ],
+ [
+ 'tables' => [
+ 'slots',
+ 'slot_roles',
+ ],
+ 'fields' => [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'slot_role_id',
+ 'role_name',
+ ],
+ 'joins' => [
+ 'slot_roles' => [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ],
+ ],
+ ]
+ ];
+ yield 'MCR read-new, content option' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+ ],
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots',
+ 'content',
+ ],
+ 'fields' => [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'slot_role_id',
+ 'content_size',
+ 'content_sha1',
+ 'content_address',
+ 'content_model',
+ ],
+ 'joins' => [
+ 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+ ],
+ ]
+ ];
+ yield 'MCR read-new, content and model options' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
+ ],
+ [ 'content', 'model' ],
+ [
+ 'tables' => [
+ 'slots',
+ 'content',
+ 'content_models',
+ ],
+ 'fields' => [
+ 'slot_revision_id',
+ 'slot_content_id',
+ 'slot_origin',
+ 'slot_role_id',
+ 'content_size',
+ 'content_sha1',
+ 'content_address',
+ 'content_model',
+ 'model_name',
+ ],
+ 'joins' => [
+ 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+ 'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ],
+ ],
+ ]
+ ];
+
+ $db = wfGetDB( DB_REPLICA );
+
+ yield 'MCR write-both/read-old' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ ],
+ [],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield 'MCR write-both/read-old, content' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ ],
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+ 'content_size' => 'slots.rev_len',
+ 'content_sha1' => 'slots.rev_sha1',
+ 'content_address' => $db->buildConcat( [
+ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+ 'model_name' => 'slots.rev_content_model',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield 'MCR write-both/read-old, content, model, role' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
+ ],
+ [ 'content', 'model', 'role' ],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+ 'content_size' => 'slots.rev_len',
+ 'content_sha1' => 'slots.rev_sha1',
+ 'content_address' => $db->buildConcat( [
+ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+ 'model_name' => 'slots.rev_content_model',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield 'pre-MCR' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_OLD,
+ ],
+ [],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ yield 'pre-MCR, content' => [
+ [
+ 'wgMultiContentRevisionSchemaMigrationStage'
+ => SCHEMA_COMPAT_OLD,
+ ],
+ [ 'content' ],
+ [
+ 'tables' => [
+ 'slots' => 'revision',
+ ],
+ 'fields' => array_merge(
+ [
+ 'slot_revision_id' => 'slots.rev_id',
+ 'slot_content_id' => 'NULL',
+ 'slot_origin' => 'slots.rev_id',
+ 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
+ 'content_size' => 'slots.rev_len',
+ 'content_sha1' => 'slots.rev_sha1',
+ 'content_address' =>
+ $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
+ 'model_name' => 'slots.rev_content_model',
+ ]
+ ),
+ 'joins' => [],
+ ]
+ ];
+ }
+
+ public function provideSelectFields() {
+ yield 'with model, comment, and actor' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ ],
+ 'fields' => array_merge(
+ [
+ 'rev_id',
+ 'rev_page',
+ 'rev_text_id',
+ 'rev_timestamp',
+ 'rev_user_text',
+ 'rev_user',
+ 'rev_actor' => 'NULL',
+ 'rev_minor_edit',
+ 'rev_deleted',
+ 'rev_len',
+ 'rev_parent_id',
+ 'rev_sha1',
+ ],
+ $this->getContentHandlerQueryFields( 'rev' ),
+ [
+ 'rev_comment_old' => 'rev_comment',
+ 'rev_comment_pk' => 'rev_id',
+ ]
+ ),
+ ];
+ yield 'no mode, no comment, no actor' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ 'fields' => array_merge(
+ [
+ 'rev_id',
+ 'rev_page',
+ 'rev_text_id',
+ 'rev_timestamp',
+ 'rev_user_text',
+ 'rev_user',
+ 'rev_actor' => 'NULL',
+ 'rev_minor_edit',
+ 'rev_deleted',
+ 'rev_len',
+ 'rev_parent_id',
+ 'rev_sha1',
+ ],
+ $this->getOldCommentQueryFields( 'rev' )
+ ),
+ ];
+ }
+
+ public function provideSelectArchiveFields() {
+ yield 'with model, comment, and actor' => [
+ [
+ 'wgContentHandlerUseDB' => true,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
+ ],
+ 'fields' => array_merge(
+ [
+ 'ar_id',
+ 'ar_page_id',
+ 'ar_rev_id',
+ 'ar_text_id',
+ 'ar_timestamp',
+ 'ar_user_text',
+ 'ar_user',
+ 'ar_actor' => 'NULL',
+ 'ar_minor_edit',
+ 'ar_deleted',
+ 'ar_len',
+ 'ar_parent_id',
+ 'ar_sha1',
+ ],
+ $this->getContentHandlerQueryFields( 'ar' ),
+ [
+ 'ar_comment_old' => 'ar_comment',
+ 'ar_comment_id' => 'ar_comment_id',
+ ]
+ ),
+ ];
+ yield 'no mode, no comment, no actor' => [
+ [
+ 'wgContentHandlerUseDB' => false,
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ],
+ 'fields' => array_merge(
+ [
+ 'ar_id',
+ 'ar_page_id',
+ 'ar_rev_id',
+ 'ar_text_id',
+ 'ar_timestamp',
+ 'ar_user_text',
+ 'ar_user',
+ 'ar_actor' => 'NULL',
+ 'ar_minor_edit',
+ 'ar_deleted',
+ 'ar_len',
+ 'ar_parent_id',
+ 'ar_sha1',
+ ],
+ $this->getOldCommentQueryFields( 'ar' )
+ ),
+ ];
+ }
+
+ /**
+ * @dataProvider provideSelectFields
+ * @covers Revision::selectFields
+ */
+ public function testRevisionSelectFields( $migrationStageSettings, $expected ) {
+ $this->setMwGlobals( $migrationStageSettings );
+ $this->overrideMwServices();
+
+ $this->hideDeprecated( 'Revision::selectFields' );
+ $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectFields() );
+ }
+
+ /**
+ * @dataProvider provideSelectArchiveFields
+ * @covers Revision::selectArchiveFields
+ */
+ public function testRevisionSelectArchiveFields( $migrationStageSettings, $expected ) {
+ $this->setMwGlobals( $migrationStageSettings );
+ $this->overrideMwServices();
+
+ $this->hideDeprecated( 'Revision::selectArchiveFields' );
+ $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectArchiveFields() );
+ }
+
+ /**
+ * @covers Revision::userJoinCond
+ */
+ public function testRevisionUserJoinCond() {
+ $this->hideDeprecated( 'Revision::userJoinCond' );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+ $this->overrideMwServices();
+ $this->assertEquals(
+ [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+ Revision::userJoinCond()
+ );
+ }
+
+ /**
+ * @covers Revision::pageJoinCond
+ */
+ public function testRevisionPageJoinCond() {
+ $this->hideDeprecated( 'Revision::pageJoinCond' );
+ $this->assertEquals(
+ [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+ Revision::pageJoinCond()
+ );
+ }
+
+ /**
+ * @covers Revision::selectTextFields
+ */
+ public function testRevisionSelectTextFields() {
+ $this->hideDeprecated( 'Revision::selectTextFields' );
+ $this->assertEquals(
+ $this->getTextQueryFields(),
+ Revision::selectTextFields()
+ );
+ }
+
+ /**
+ * @covers Revision::selectPageFields
+ */
+ public function testRevisionSelectPageFields() {
+ $this->hideDeprecated( 'Revision::selectPageFields' );
+ $this->assertEquals(
+ $this->getPageQueryFields(),
+ Revision::selectPageFields()
+ );
+ }
+
+ /**
+ * @covers Revision::selectUserFields
+ */
+ public function testRevisionSelectUserFields() {
+ $this->hideDeprecated( 'Revision::selectUserFields' );
+ $this->assertEquals(
+ $this->getUserQueryFields(),
+ Revision::selectUserFields()
+ );
+ }
+
+ /**
+ * @covers Revision::getArchiveQueryInfo
+ * @dataProvider provideArchiveQueryInfo
+ */
+ public function testRevisionGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
+ $this->setMwGlobals( $migrationStageSettings );
+ $this->overrideMwServices();
+
+ $queryInfo = Revision::getArchiveQueryInfo();
+ $this->assertQueryInfoEquals( $expected, $queryInfo );
+ }
+
+ /**
+ * @covers Revision::getQueryInfo
+ * @dataProvider provideQueryInfo
+ */
+ public function testRevisionGetQueryInfo( $migrationStageSettings, $options, $expected ) {
+ $this->setMwGlobals( $migrationStageSettings );
+ $this->overrideMwServices();
+
+ $queryInfo = Revision::getQueryInfo( $options );
+ $this->assertQueryInfoEquals( $expected, $queryInfo );
+ }
+
+ /**
+ * @dataProvider provideQueryInfo
+ * @covers \MediaWiki\Revision\RevisionStore::getQueryInfo
+ */
+ public function testRevisionStoreGetQueryInfo( $migrationStageSettings, $options, $expected ) {
+ $this->setMwGlobals( $migrationStageSettings );
+ $this->overrideMwServices();
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $queryInfo = $store->getQueryInfo( $options );
+ $this->assertQueryInfoEquals( $expected, $queryInfo );
+ }
+
+ /**
+ * @dataProvider provideSlotsQueryInfo
+ * @covers \MediaWiki\Revision\RevisionStore::getSlotsQueryInfo
+ */
+ public function testRevisionStoreGetSlotsQueryInfo(
+ $migrationStageSettings,
+ $options,
+ $expected
+ ) {
+ $this->setMwGlobals( $migrationStageSettings );
+ $this->overrideMwServices();
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $queryInfo = $store->getSlotsQueryInfo( $options );
+ $this->assertQueryInfoEquals( $expected, $queryInfo );
+ }
+
+ /**
+ * @dataProvider provideArchiveQueryInfo
+ * @covers \MediaWiki\Revision\RevisionStore::getArchiveQueryInfo
+ */
+ public function testRevisionStoreGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
+ $this->setMwGlobals( $migrationStageSettings );
+ $this->overrideMwServices();
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $queryInfo = $store->getArchiveQueryInfo();
+ $this->assertQueryInfoEquals( $expected, $queryInfo );
+ }
+
+ private function assertQueryInfoEquals( $expected, $queryInfo ) {
+ $this->assertArrayEqualsIgnoringIntKeyOrder(
+ $expected['tables'],
+ $queryInfo['tables'],
+ 'tables'
+ );
+ $this->assertArrayEqualsIgnoringIntKeyOrder(
+ $expected['fields'],
+ $queryInfo['fields'],
+ 'fields'
+ );
+ $this->assertArrayEqualsIgnoringIntKeyOrder(
+ $expected['joins'],
+ $queryInfo['joins'],
+ 'joins'
+ );
+ }
+
+ /**
+ * Assert that the two arrays passed are equal, ignoring the order of the values that integer
+ * keys.
+ *
+ * Note: Failures of this assertion can be slightly confusing as the arrays are actually
+ * split into a string key array and an int key array before assertions occur.
+ *
+ * @param array $expected
+ * @param array $actual
+ */
+ private function assertArrayEqualsIgnoringIntKeyOrder(
+ array $expected,
+ array $actual,
+ $message = null
+ ) {
+ $this->objectAssociativeSort( $expected );
+ $this->objectAssociativeSort( $actual );
+
+ // Separate the int key values from the string key values so that assertion failures are
+ // easier to understand.
+ $expectedIntKeyValues = [];
+ $actualIntKeyValues = [];
+
+ // Remove all int keys and re add them at the end after sorting by value
+ // This will result in all int keys being in the same order with same ints at the end of
+ // the array
+ foreach ( $expected as $key => $value ) {
+ if ( is_int( $key ) ) {
+ unset( $expected[$key] );
+ $expectedIntKeyValues[] = $value;
+ }
+ }
+ foreach ( $actual as $key => $value ) {
+ if ( is_int( $key ) ) {
+ unset( $actual[$key] );
+ $actualIntKeyValues[] = $value;
+ }
+ }
+
+ $this->objectAssociativeSort( $expected );
+ $this->objectAssociativeSort( $actual );
+
+ $this->objectAssociativeSort( $expectedIntKeyValues );
+ $this->objectAssociativeSort( $actualIntKeyValues );
+
+ $this->assertEquals( $expected, $actual, $message );
+ $this->assertEquals( $expectedIntKeyValues, $actualIntKeyValues, $message );
+ }
+
+}
--- /dev/null
+<?php
+
+// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClassTrait
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use LogicException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use MediaWiki\User\UserIdentityValue;
+use TextContent;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\RevisionRecord
+ *
+ * @note Expects to be used in classes that extend MediaWikiTestCase.
+ */
+trait RevisionRecordTests {
+
+ /**
+ * @param array $rowOverrides
+ *
+ * @return RevisionRecord
+ */
+ protected abstract function newRevision( array $rowOverrides = [] );
+
+ private function provideAudienceCheckData( $field ) {
+ yield 'field accessible for oversighter (ALL)' => [
+ RevisionRecord::SUPPRESSED_ALL,
+ [ 'oversight' ],
+ true,
+ false
+ ];
+
+ yield 'field accessible for oversighter' => [
+ RevisionRecord::DELETED_RESTRICTED | $field,
+ [ 'oversight' ],
+ true,
+ false
+ ];
+
+ yield 'field not accessible for sysops (ALL)' => [
+ RevisionRecord::SUPPRESSED_ALL,
+ [ 'sysop' ],
+ false,
+ false
+ ];
+
+ yield 'field not accessible for sysops' => [
+ RevisionRecord::DELETED_RESTRICTED | $field,
+ [ 'sysop' ],
+ false,
+ false
+ ];
+
+ yield 'field accessible for sysops' => [
+ $field,
+ [ 'sysop' ],
+ true,
+ false
+ ];
+
+ yield 'field suppressed for logged in users' => [
+ $field,
+ [ 'user' ],
+ false,
+ false
+ ];
+
+ yield 'unrelated field suppressed' => [
+ $field === RevisionRecord::DELETED_COMMENT
+ ? RevisionRecord::DELETED_USER
+ : RevisionRecord::DELETED_COMMENT,
+ [ 'user' ],
+ true,
+ true
+ ];
+
+ yield 'nothing suppressed' => [
+ 0,
+ [ 'user' ],
+ true,
+ true
+ ];
+ }
+
+ public function testSerialization_fails() {
+ $this->setExpectedException( LogicException::class );
+ $rev = $this->newRevision();
+ serialize( $rev );
+ }
+
+ public function provideGetComment_audience() {
+ return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
+ }
+
+ private function forceStandardPermissions() {
+ $this->setMwGlobals(
+ 'wgGroupPermissions',
+ [
+ 'user' => [
+ 'viewsuppressed' => false,
+ 'suppressrevision' => false,
+ 'deletedtext' => false,
+ 'deletedhistory' => false,
+ ],
+ 'sysop' => [
+ 'viewsuppressed' => false,
+ 'suppressrevision' => false,
+ 'deletedtext' => true,
+ 'deletedhistory' => true,
+ ],
+ 'oversight' => [
+ 'deletedtext' => true,
+ 'deletedhistory' => true,
+ 'viewsuppressed' => true,
+ 'suppressrevision' => true,
+ ],
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider provideGetComment_audience
+ */
+ public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
+ $this->forceStandardPermissions();
+
+ $user = $this->getTestUser( $groups )->getUser();
+ $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+ $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
+
+ $this->assertSame(
+ $publicCan,
+ $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
+ 'public can'
+ );
+ $this->assertSame(
+ $userCan,
+ $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
+ 'user can'
+ );
+ }
+
+ public function provideGetUser_audience() {
+ return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
+ }
+
+ /**
+ * @dataProvider provideGetUser_audience
+ */
+ public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
+ $this->forceStandardPermissions();
+
+ $user = $this->getTestUser( $groups )->getUser();
+ $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+ $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
+
+ $this->assertSame(
+ $publicCan,
+ $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
+ 'public can'
+ );
+ $this->assertSame(
+ $userCan,
+ $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
+ 'user can'
+ );
+ }
+
+ public function provideGetSlot_audience() {
+ return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
+ }
+
+ /**
+ * @dataProvider provideGetSlot_audience
+ */
+ public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
+ $this->forceStandardPermissions();
+
+ $user = $this->getTestUser( $groups )->getUser();
+ $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+ // NOTE: slot meta-data is never suppressed, just the content is!
+ $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ), 'hasSlot is never suppressed' );
+ $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw meta' );
+ $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
+ 'public meta' );
+
+ $this->assertNotNull(
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
+ 'user can'
+ );
+
+ try {
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
+ $exception = null;
+ } catch ( SuppressedDataException $ex ) {
+ $exception = $ex;
+ }
+
+ $this->assertSame(
+ $publicCan,
+ $exception === null,
+ 'public can'
+ );
+
+ try {
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
+ $exception = null;
+ } catch ( SuppressedDataException $ex ) {
+ $exception = $ex;
+ }
+
+ $this->assertSame(
+ $userCan,
+ $exception === null,
+ 'user can'
+ );
+ }
+
+ /**
+ * @dataProvider provideGetSlot_audience
+ */
+ public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
+ $this->forceStandardPermissions();
+
+ $user = $this->getTestUser( $groups )->getUser();
+ $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
+
+ $this->assertNotNull( $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
+
+ $this->assertSame(
+ $publicCan,
+ $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ) !== null,
+ 'public can'
+ );
+ $this->assertSame(
+ $userCan,
+ $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ) !== null,
+ 'user can'
+ );
+ }
+
+ public function testGetSlot() {
+ $rev = $this->newRevision();
+
+ $slot = $rev->getSlot( SlotRecord::MAIN );
+ $this->assertNotNull( $slot, 'getSlot()' );
+ $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
+ }
+
+ public function testHasSlot() {
+ $rev = $this->newRevision();
+
+ $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ) );
+ $this->assertFalse( $rev->hasSlot( 'xyz' ) );
+ }
+
+ public function testGetContent() {
+ $rev = $this->newRevision();
+
+ $content = $rev->getSlot( SlotRecord::MAIN );
+ $this->assertNotNull( $content, 'getContent()' );
+ $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
+ }
+
+ public function provideUserCanBitfield() {
+ yield [ 0, 0, [], null, true ];
+ // Bitfields match, user has no permissions
+ yield [
+ RevisionRecord::DELETED_TEXT,
+ RevisionRecord::DELETED_TEXT,
+ [],
+ null,
+ false
+ ];
+ yield [
+ RevisionRecord::DELETED_COMMENT,
+ RevisionRecord::DELETED_COMMENT,
+ [],
+ null,
+ false,
+ ];
+ yield [
+ RevisionRecord::DELETED_USER,
+ RevisionRecord::DELETED_USER,
+ [],
+ null,
+ false
+ ];
+ yield [
+ RevisionRecord::DELETED_RESTRICTED,
+ RevisionRecord::DELETED_RESTRICTED,
+ [],
+ null,
+ false,
+ ];
+ // Bitfields match, user (admin) does have permissions
+ yield [
+ RevisionRecord::DELETED_TEXT,
+ RevisionRecord::DELETED_TEXT,
+ [ 'sysop' ],
+ null,
+ true,
+ ];
+ yield [
+ RevisionRecord::DELETED_COMMENT,
+ RevisionRecord::DELETED_COMMENT,
+ [ 'sysop' ],
+ null,
+ true,
+ ];
+ yield [
+ RevisionRecord::DELETED_USER,
+ RevisionRecord::DELETED_USER,
+ [ 'sysop' ],
+ null,
+ true,
+ ];
+ // Bitfields match, user (admin) does not have permissions
+ yield [
+ RevisionRecord::DELETED_RESTRICTED,
+ RevisionRecord::DELETED_RESTRICTED,
+ [ 'sysop' ],
+ null,
+ false,
+ ];
+ // Bitfields match, user (oversight) does have permissions
+ yield [
+ RevisionRecord::DELETED_RESTRICTED,
+ RevisionRecord::DELETED_RESTRICTED,
+ [ 'oversight' ],
+ null,
+ true,
+ ];
+ // Check permissions using the title
+ yield [
+ RevisionRecord::DELETED_TEXT,
+ RevisionRecord::DELETED_TEXT,
+ [ 'sysop' ],
+ __METHOD__,
+ true,
+ ];
+ yield [
+ RevisionRecord::DELETED_TEXT,
+ RevisionRecord::DELETED_TEXT,
+ [],
+ __METHOD__,
+ false,
+ ];
+ }
+
+ /**
+ * @dataProvider provideUserCanBitfield
+ * @covers \MediaWiki\Revision\RevisionRecord::userCanBitfield
+ */
+ public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
+ if ( is_string( $title ) ) {
+ // NOTE: Data providers cannot instantiate Title objects! See T202641.
+ $title = Title::newFromText( $title );
+ }
+
+ $this->forceStandardPermissions();
+
+ $user = $this->getTestUser( $userGroups )->getUser();
+
+ $this->assertSame(
+ $expected,
+ RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
+ );
+ }
+
+ public function provideHasSameContent() {
+ // Create some slots with content
+ $mainA = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'A' ) );
+ $mainB = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'B' ) );
+ $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
+ $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
+
+ $initialRecordSpec = [ [ $mainA ], 12 ];
+
+ return [
+ 'same record object' => [
+ true,
+ $initialRecordSpec,
+ $initialRecordSpec,
+ ],
+ 'same record content, different object' => [
+ true,
+ [ [ $mainA ], 12 ],
+ [ [ $mainA ], 13 ],
+ ],
+ 'same record content, aux slot, different object' => [
+ true,
+ [ [ $auxA ], 12 ],
+ [ [ $auxB ], 13 ],
+ ],
+ 'different content' => [
+ false,
+ [ [ $mainA ], 12 ],
+ [ [ $mainB ], 13 ],
+ ],
+ 'different content and number of slots' => [
+ false,
+ [ [ $mainA ], 12 ],
+ [ [ $mainA, $mainB ], 13 ],
+ ],
+ ];
+ }
+
+ /**
+ * @note Do not call directly from a data provider! Data providers cannot instantiate
+ * Title objects! See T202641.
+ *
+ * @param SlotRecord[] $slots
+ * @param int $revId
+ * @return RevisionStoreRecord
+ */
+ private function makeHasSameContentTestRecord( array $slots, $revId ) {
+ $title = Title::newFromText( 'provideHasSameContent' );
+ $title->resetArticleID( 19 );
+ $slots = new RevisionSlots( $slots );
+
+ return new RevisionStoreRecord(
+ $title,
+ new UserIdentityValue( 11, __METHOD__, 0 ),
+ CommentStoreComment::newUnsavedComment( __METHOD__ ),
+ (object)[
+ 'rev_id' => strval( $revId ),
+ 'rev_page' => strval( $title->getArticleID() ),
+ 'rev_timestamp' => '20200101000000',
+ 'rev_deleted' => 0,
+ 'rev_minor_edit' => 0,
+ 'rev_parent_id' => '5',
+ 'rev_len' => $slots->computeSize(),
+ 'rev_sha1' => $slots->computeSha1(),
+ 'page_latest' => '18',
+ ],
+ $slots
+ );
+ }
+
+ /**
+ * @dataProvider provideHasSameContent
+ * @covers \MediaWiki\Revision\RevisionRecord::hasSameContent
+ * @group Database
+ */
+ public function testHasSameContent(
+ $expected,
+ $recordSpec1,
+ $recordSpec2
+ ) {
+ $record1 = $this->makeHasSameContentTestRecord( ...$recordSpec1 );
+ $record2 = $this->makeHasSameContentTestRecord( ...$recordSpec2 );
+
+ $this->assertSame(
+ $expected,
+ $record1->hasSameContent( $record2 )
+ );
+ }
+
+ public function provideIsDeleted() {
+ yield 'no deletion' => [
+ 0,
+ [
+ RevisionRecord::DELETED_TEXT => false,
+ RevisionRecord::DELETED_COMMENT => false,
+ RevisionRecord::DELETED_USER => false,
+ RevisionRecord::DELETED_RESTRICTED => false,
+ ]
+ ];
+ yield 'text deleted' => [
+ RevisionRecord::DELETED_TEXT,
+ [
+ RevisionRecord::DELETED_TEXT => true,
+ RevisionRecord::DELETED_COMMENT => false,
+ RevisionRecord::DELETED_USER => false,
+ RevisionRecord::DELETED_RESTRICTED => false,
+ ]
+ ];
+ yield 'text and comment deleted' => [
+ RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
+ [
+ RevisionRecord::DELETED_TEXT => true,
+ RevisionRecord::DELETED_COMMENT => true,
+ RevisionRecord::DELETED_USER => false,
+ RevisionRecord::DELETED_RESTRICTED => false,
+ ]
+ ];
+ yield 'all 4 deleted' => [
+ RevisionRecord::DELETED_TEXT +
+ RevisionRecord::DELETED_COMMENT +
+ RevisionRecord::DELETED_RESTRICTED +
+ RevisionRecord::DELETED_USER,
+ [
+ RevisionRecord::DELETED_TEXT => true,
+ RevisionRecord::DELETED_COMMENT => true,
+ RevisionRecord::DELETED_USER => true,
+ RevisionRecord::DELETED_RESTRICTED => true,
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider provideIsDeleted
+ * @covers \MediaWiki\Revision\RevisionRecord::isDeleted
+ */
+ public function testIsDeleted( $revDeleted, $assertionMap ) {
+ $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
+ foreach ( $assertionMap as $deletionLevel => $expected ) {
+ $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
+ }
+ }
+
+ public function testIsReadyForInsertion() {
+ $rev = $this->newRevision();
+ $this->assertTrue( $rev->isReadyForInsertion() );
+ }
+
+}
use Content;
use Language;
use LogicException;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionRenderer;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentityValue;
+use MediaWiki\Revision\SlotRecord;
use MediaWikiTestCase;
+use MediaWiki\User\UserIdentityValue;
use ParserOptions;
use ParserOutput;
use PHPUnit\Framework\MockObject\MockObject;
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\SlotRecord;
+use MediaWikiTestCase;
+use TextContent;
+use WikitextContent;
+
+class RevisionSlotsTest extends MediaWikiTestCase {
+
+ /**
+ * @param SlotRecord[] $slots
+ * @return RevisionSlots
+ */
+ protected function newRevisionSlots( $slots = [] ) {
+ return new RevisionSlots( $slots );
+ }
+
+ public function provideConstructorFailue() {
+ yield 'not an array or callable' => [
+ 'foo'
+ ];
+ yield 'array of the wrong thing' => [
+ [ 1, 2, 3 ]
+ ];
+ }
+
+ /**
+ * @dataProvider provideConstructorFailue
+ * @param $slots
+ *
+ * @covers \MediaWiki\Revision\RevisionSlots::__construct
+ * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal
+ */
+ public function testConstructorFailue( $slots ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+
+ new RevisionSlots( $slots );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::getSlot
+ */
+ public function testGetSlot() {
+ $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+ $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+ $this->assertSame( $mainSlot, $slots->getSlot( SlotRecord::MAIN ) );
+ $this->assertSame( $auxSlot, $slots->getSlot( 'aux' ) );
+ $this->setExpectedException( RevisionAccessException::class );
+ $slots->getSlot( 'nothere' );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::hasSlot
+ */
+ public function testHasSlot() {
+ $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+ $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+ $this->assertTrue( $slots->hasSlot( SlotRecord::MAIN ) );
+ $this->assertTrue( $slots->hasSlot( 'aux' ) );
+ $this->assertFalse( $slots->hasSlot( 'AUX' ) );
+ $this->assertFalse( $slots->hasSlot( 'xyz' ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::getContent
+ */
+ public function testGetContent() {
+ $mainContent = new WikitextContent( 'A' );
+ $auxContent = new WikitextContent( 'B' );
+ $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, $mainContent );
+ $auxSlot = SlotRecord::newUnsaved( 'aux', $auxContent );
+ $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+ $this->assertSame( $mainContent, $slots->getContent( SlotRecord::MAIN ) );
+ $this->assertSame( $auxContent, $slots->getContent( 'aux' ) );
+ $this->setExpectedException( RevisionAccessException::class );
+ $slots->getContent( 'nothere' );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::getSlotRoles
+ */
+ public function testGetSlotRoles_someSlots() {
+ $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+ $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
+
+ $this->assertSame( [ 'main', 'aux' ], $slots->getSlotRoles() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::getSlotRoles
+ */
+ public function testGetSlotRoles_noSlots() {
+ $slots = $this->newRevisionSlots( [] );
+
+ $this->assertSame( [], $slots->getSlotRoles() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::getSlots
+ */
+ public function testGetSlots() {
+ $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
+ $slotsArray = [ $mainSlot, $auxSlot ];
+ $slots = $this->newRevisionSlots( $slotsArray );
+
+ $this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::getInheritedSlots
+ */
+ public function testGetInheritedSlots() {
+ $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $auxSlot = SlotRecord::newInherited(
+ SlotRecord::newSaved(
+ 7, 7, 'foo',
+ SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
+ )
+ );
+ $slotsArray = [ $mainSlot, $auxSlot ];
+ $slots = $this->newRevisionSlots( $slotsArray );
+
+ $this->assertEquals( [ 'aux' => $auxSlot ], $slots->getInheritedSlots() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionSlots::getOriginalSlots
+ */
+ public function testGetOriginalSlots() {
+ $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $auxSlot = SlotRecord::newInherited(
+ SlotRecord::newSaved(
+ 7, 7, 'foo',
+ SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
+ )
+ );
+ $slotsArray = [ $mainSlot, $auxSlot ];
+ $slots = $this->newRevisionSlots( $slotsArray );
+
+ $this->assertEquals( [ 'main' => $mainSlot ], $slots->getOriginalSlots() );
+ }
+
+ public function provideComputeSize() {
+ yield [ 1, [ 'A' ] ];
+ yield [ 2, [ 'AA' ] ];
+ yield [ 4, [ 'AA', 'X', 'H' ] ];
+ }
+
+ /**
+ * @dataProvider provideComputeSize
+ * @covers \MediaWiki\Revision\RevisionSlots::computeSize
+ */
+ public function testComputeSize( $expected, $contentStrings ) {
+ $slotsArray = [];
+ foreach ( $contentStrings as $key => $contentString ) {
+ $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
+ }
+ $slots = $this->newRevisionSlots( $slotsArray );
+
+ $this->assertSame( $expected, $slots->computeSize() );
+ }
+
+ public function provideComputeSha1() {
+ yield [ 'ctqm7794fr2dp1taki8a88ovwnvmnmj', [ 'A' ] ];
+ yield [ 'eyq8wiwlcofnaiy4eid97gyfy60uw51', [ 'AA' ] ];
+ yield [ 'lavctqfpxartyjr31f853drgfl4kj1g', [ 'AA', 'X', 'H' ] ];
+ }
+
+ /**
+ * @dataProvider provideComputeSha1
+ * @covers \MediaWiki\Revision\RevisionSlots::computeSha1
+ * @note this test is a bit brittle as the hashes are hardcoded, perhaps just check that strings
+ * are returned and different Slots objects return different strings?
+ */
+ public function testComputeSha1( $expected, $contentStrings ) {
+ $slotsArray = [];
+ foreach ( $contentStrings as $key => $contentString ) {
+ $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
+ }
+ $slots = $this->newRevisionSlots( $slotsArray );
+
+ $this->assertSame( $expected, $slots->computeSha1() );
+ }
+
+ public function provideHasSameContent() {
+ $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
+ $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
+ $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
+ $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
+ $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
+
+ $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+ $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+ $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
+ $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
+ $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
+
+ yield 'same instance' => [ $a, $a, true ];
+ yield 'same slots' => [ $a, $a2, true ];
+ yield 'same content' => [ $a, $a3, true ];
+
+ yield 'different roles' => [ $a, $b, false ];
+ yield 'different content' => [ $a, $c, false ];
+ }
+
+ /**
+ * @dataProvider provideHasSameContent
+ * @covers \MediaWiki\Revision\RevisionSlots::hasSameContent
+ */
+ public function testHasSameContent( RevisionSlots $a, RevisionSlots $b, $same ) {
+ $this->assertSame( $same, $a->hasSameContent( $b ) );
+ $this->assertSame( $same, $b->hasSameContent( $a ) );
+ }
+
+ public function provideGetRolesWithDifferentContent() {
+ $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
+ $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
+ $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
+ $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
+ $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
+
+ $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+ $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
+ $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
+ $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
+ $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
+
+ yield 'same instance' => [ $a, $a, [] ];
+ yield 'same slots' => [ $a, $a2, [] ];
+ yield 'same content' => [ $a, $a3, [] ];
+
+ yield 'different roles' => [ $a, $b, [ 'x', 'y' ] ];
+ yield 'different content' => [ $a, $c, [ 'z' ] ];
+ }
+
+ /**
+ * @dataProvider provideGetRolesWithDifferentContent
+ * @covers \MediaWiki\Revision\RevisionSlots::getRolesWithDifferentContent
+ */
+ public function testGetRolesWithDifferentContent( RevisionSlots $a, RevisionSlots $b, $roles ) {
+ $this->assertArrayEquals( $roles, $a->getRolesWithDifferentContent( $b ) );
+ $this->assertArrayEquals( $roles, $b->getRolesWithDifferentContent( $a ) );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use Content;
+use Exception;
+use HashBagOStuff;
+use InvalidArgumentException;
+use Language;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use PHPUnit_Framework_MockObject_MockObject;
+use Revision;
+use TestUserRegistry;
+use Title;
+use WANObjectCache;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseSqlite;
+use Wikimedia\Rdbms\FakeResultWrapper;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\TransactionProfiler;
+use WikiPage;
+use WikitextContent;
+
+/**
+ * @group Database
+ * @group RevisionStore
+ */
+abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
+
+ /**
+ * @var Title
+ */
+ private $testPageTitle;
+
+ /**
+ * @var WikiPage
+ */
+ private $testPage;
+
+ /**
+ * @return int
+ */
+ abstract protected function getMcrMigrationStage();
+
+ /**
+ * @return bool
+ */
+ protected function getContentHandlerUseDB() {
+ return true;
+ }
+
+ /**
+ * @return string[]
+ */
+ abstract protected function getMcrTablesToReset();
+
+ public function setUp() {
+ parent::setUp();
+ $this->tablesUsed[] = 'archive';
+ $this->tablesUsed[] = 'page';
+ $this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'comment';
+
+ $this->tablesUsed += $this->getMcrTablesToReset();
+
+ $this->setMwGlobals( [
+ 'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
+ 'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
+ 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
+ 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
+ ] );
+
+ $this->overrideMwServices();
+ }
+
+ protected function addCoreDBData() {
+ // Blank out. This would fail with a modified schema, and we don't need it.
+ }
+
+ /**
+ * @return Title
+ */
+ protected function getTestPageTitle() {
+ if ( $this->testPageTitle ) {
+ return $this->testPageTitle;
+ }
+
+ $this->testPageTitle = Title::newFromText( 'UTPage-' . __CLASS__ );
+ return $this->testPageTitle;
+ }
+ /**
+ * @return WikiPage
+ */
+ protected function getTestPage() {
+ if ( $this->testPage ) {
+ return $this->testPage;
+ }
+
+ $title = $this->getTestPageTitle();
+ $this->testPage = WikiPage::factory( $title );
+
+ if ( !$this->testPage->exists() ) {
+ // Make sure we don't write to the live db.
+ $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
+
+ $user = static::getTestSysop()->getUser();
+
+ $this->testPage->doEditContent(
+ new WikitextContent( 'UTContent-' . __CLASS__ ),
+ 'UTPageSummary-' . __CLASS__,
+ EDIT_NEW | EDIT_SUPPRESS_RC,
+ false,
+ $user
+ );
+ }
+
+ return $this->testPage;
+ }
+
+ /**
+ * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getLoadBalancerMock( array $server ) {
+ $lb = $this->getMockBuilder( LoadBalancer::class )
+ ->setMethods( [ 'reallyOpenConnection' ] )
+ ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] )
+ ->getMock();
+
+ $lb->method( 'reallyOpenConnection' )->willReturnCallback(
+ function ( array $server, $dbNameOverride ) {
+ return $this->getDatabaseMock( $server );
+ }
+ );
+
+ return $lb;
+ }
+
+ /**
+ * @return Database|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getDatabaseMock( array $params ) {
+ $db = $this->getMockBuilder( DatabaseSqlite::class )
+ ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] )
+ ->setConstructorArgs( [ $params ] )
+ ->getMock();
+
+ $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) );
+ $db->method( 'isOpen' )->willReturn( true );
+
+ return $db;
+ }
+
+ public function provideDomainCheck() {
+ yield [ false, 'test', '' ];
+ yield [ 'test', 'test', '' ];
+
+ yield [ false, 'test', 'foo_' ];
+ yield [ 'test-foo_', 'test', 'foo_' ];
+
+ yield [ false, 'dash-test', '' ];
+ yield [ 'dash-test', 'dash-test', '' ];
+
+ yield [ false, 'underscore_test', 'foo_' ];
+ yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ];
+ }
+
+ /**
+ * @dataProvider provideDomainCheck
+ * @covers \MediaWiki\Revision\RevisionStore::checkDatabaseWikiId
+ */
+ public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
+ $this->setMwGlobals(
+ [
+ 'wgDBname' => $dbName,
+ 'wgDBprefix' => $dbPrefix,
+ ]
+ );
+
+ $loadBalancer = $this->getLoadBalancerMock(
+ [
+ 'host' => '*dummy*',
+ 'dbDirectory' => '*dummy*',
+ 'user' => 'test',
+ 'password' => 'test',
+ 'flags' => 0,
+ 'variables' => [],
+ 'schema' => '',
+ 'cliMode' => true,
+ 'agent' => '',
+ 'load' => 100,
+ 'profiler' => null,
+ 'trxProfiler' => new TransactionProfiler(),
+ 'connLogger' => new \Psr\Log\NullLogger(),
+ 'queryLogger' => new \Psr\Log\NullLogger(),
+ 'errorLogger' => function () {
+ },
+ 'deprecationLogger' => function () {
+ },
+ 'type' => 'test',
+ 'dbname' => $dbName,
+ 'tablePrefix' => $dbPrefix,
+ ]
+ );
+ $db = $loadBalancer->getConnection( DB_REPLICA );
+
+ /** @var SqlBlobStore $blobStore */
+ $blobStore = $this->getMockBuilder( SqlBlobStore::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $store = new RevisionStore(
+ $loadBalancer,
+ $blobStore,
+ new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
+ MediaWikiServices::getInstance()->getCommentStore(),
+ MediaWikiServices::getInstance()->getContentModelStore(),
+ MediaWikiServices::getInstance()->getSlotRoleStore(),
+ $this->getMcrMigrationStage(),
+ MediaWikiServices::getInstance()->getActorMigration(),
+ $wikiId
+ );
+
+ $count = $store->countRevisionsByPageId( $db, 0 );
+
+ // Dummy check to make PhpUnit happy. We are really only interested in
+ // countRevisionsByPageId not failing due to the DB domain check.
+ $this->assertSame( 0, $count );
+ }
+
+ protected function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) {
+ $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() );
+ $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() );
+ $this->assertEquals( $l1->getFragment(), $l2->getFragment() );
+ $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() );
+ }
+
+ protected function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) {
+ $this->assertEquals(
+ $r1->getPageAsLinkTarget()->getNamespace(),
+ $r2->getPageAsLinkTarget()->getNamespace()
+ );
+
+ $this->assertEquals(
+ $r1->getPageAsLinkTarget()->getText(),
+ $r2->getPageAsLinkTarget()->getText()
+ );
+
+ if ( $r1->getParentId() ) {
+ $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
+ }
+
+ $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() );
+ $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() );
+ $this->assertEquals( $r1->getComment(), $r2->getComment() );
+ $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() );
+ $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() );
+ $this->assertEquals( $r1->getSha1(), $r2->getSha1() );
+ $this->assertEquals( $r1->getSize(), $r2->getSize() );
+ $this->assertEquals( $r1->getPageId(), $r2->getPageId() );
+ $this->assertArrayEquals( $r1->getSlotRoles(), $r2->getSlotRoles() );
+ $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() );
+ $this->assertEquals( $r1->isMinor(), $r2->isMinor() );
+ foreach ( $r1->getSlotRoles() as $role ) {
+ $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) );
+ $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) );
+ }
+ foreach ( [
+ RevisionRecord::DELETED_TEXT,
+ RevisionRecord::DELETED_COMMENT,
+ RevisionRecord::DELETED_USER,
+ RevisionRecord::DELETED_RESTRICTED,
+ ] as $field ) {
+ $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) );
+ }
+ }
+
+ protected function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) {
+ $this->assertSame( $s1->getRole(), $s2->getRole() );
+ $this->assertSame( $s1->getModel(), $s2->getModel() );
+ $this->assertSame( $s1->getFormat(), $s2->getFormat() );
+ $this->assertSame( $s1->getSha1(), $s2->getSha1() );
+ $this->assertSame( $s1->getSize(), $s2->getSize() );
+ $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) );
+
+ $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null;
+ $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null;
+ }
+
+ protected function assertRevisionCompleteness( RevisionRecord $r ) {
+ $this->assertTrue( $r->hasSlot( SlotRecord::MAIN ) );
+ $this->assertInstanceOf( SlotRecord::class, $r->getSlot( SlotRecord::MAIN ) );
+ $this->assertInstanceOf( Content::class, $r->getContent( SlotRecord::MAIN ) );
+
+ foreach ( $r->getSlotRoles() as $role ) {
+ $this->assertSlotCompleteness( $r, $r->getSlot( $role ) );
+ }
+ }
+
+ protected function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
+ $this->assertTrue( $slot->hasAddress() );
+ $this->assertSame( $r->getId(), $slot->getRevision() );
+
+ $this->assertInstanceOf( Content::class, $slot->getContent() );
+ }
+
+ /**
+ * @param mixed[] $details
+ *
+ * @return RevisionRecord
+ */
+ private function getRevisionRecordFromDetailsArray( $details = [] ) {
+ // Convert some values that can't be provided by dataProviders
+ if ( isset( $details['user'] ) && $details['user'] === true ) {
+ $details['user'] = $this->getTestUser()->getUser();
+ }
+ if ( isset( $details['page'] ) && $details['page'] === true ) {
+ $details['page'] = $this->getTestPage()->getId();
+ }
+ if ( isset( $details['parent'] ) && $details['parent'] === true ) {
+ $details['parent'] = $this->getTestPage()->getLatest();
+ }
+
+ // Create the RevisionRecord with any available data
+ $rev = new MutableRevisionRecord( $this->getTestPageTitle() );
+ isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null;
+ isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null;
+ isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null;
+ isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null;
+ isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null;
+ isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null;
+ isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null;
+ isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null;
+ isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null;
+ isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null;
+ isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null;
+
+ if ( isset( $details['content'] ) ) {
+ foreach ( $details['content'] as $role => $content ) {
+ $rev->setContent( $role, $content );
+ }
+ }
+
+ return $rev;
+ }
+
+ public function provideInsertRevisionOn_successes() {
+ yield 'Bare minimum revision insertion' => [
+ [
+ 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+ 'page' => true,
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ ];
+ yield 'Detailed revision insertion' => [
+ [
+ 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+ 'parent' => true,
+ 'page' => true,
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ 'minor' => true,
+ 'visibility' => RevisionRecord::DELETED_RESTRICTED,
+ ],
+ ];
+ }
+
+ protected function getRandomCommentStoreComment() {
+ return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
+ }
+
+ /**
+ * @dataProvider provideInsertRevisionOn_successes
+ * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+ * @covers \MediaWiki\Revision\RevisionStore::insertSlotRowOn
+ * @covers \MediaWiki\Revision\RevisionStore::insertContentRowOn
+ */
+ public function testInsertRevisionOn_successes(
+ array $revDetails = []
+ ) {
+ $title = $this->getTestPageTitle();
+ $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
+
+ $this->overrideMwServices();
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
+
+ // is the new revision correct?
+ $this->assertRevisionCompleteness( $return );
+ $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() );
+ $this->assertRevisionRecordsEqual( $rev, $return );
+
+ // can we load it from the store?
+ $loaded = $store->getRevisionById( $return->getId() );
+ $this->assertRevisionCompleteness( $loaded );
+ $this->assertRevisionRecordsEqual( $return, $loaded );
+
+ // can we find it directly in the database?
+ $this->assertRevisionExistsInDatabase( $return );
+ }
+
+ protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+ $row = $this->revisionToRow( new Revision( $rev ), [] );
+
+ // unset nulled fields
+ unset( $row->rev_content_model );
+ unset( $row->rev_content_format );
+
+ // unset fake fields
+ unset( $row->rev_comment_text );
+ unset( $row->rev_comment_data );
+ unset( $row->rev_comment_cid );
+ unset( $row->rev_comment_id );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $queryInfo = $store->getQueryInfo( [ 'user' ] );
+
+ $row = get_object_vars( $row );
+ $this->assertSelect(
+ $queryInfo['tables'],
+ array_keys( $row ),
+ [ 'rev_id' => $rev->getId() ],
+ [ array_values( $row ) ],
+ [],
+ $queryInfo['joins']
+ );
+ }
+
+ /**
+ * @param SlotRecord $a
+ * @param SlotRecord $b
+ */
+ protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+ // Assert that the same blob address has been used.
+ $this->assertSame( $a->getAddress(), $b->getAddress() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+ */
+ public function testInsertRevisionOn_blobAddressExists() {
+ $title = $this->getTestPageTitle();
+ $revDetails = [
+ 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+ 'parent' => true,
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ];
+
+ $this->overrideMwServices();
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ // Insert the first revision
+ $revOne = $this->getRevisionRecordFromDetailsArray( $revDetails );
+ $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) );
+ $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() );
+ $this->assertRevisionRecordsEqual( $revOne, $firstReturn );
+
+ // Insert a second revision inheriting the same blob address
+ $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( SlotRecord::MAIN ) );
+ $revTwo = $this->getRevisionRecordFromDetailsArray( $revDetails );
+ $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) );
+ $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() );
+ $this->assertRevisionRecordsEqual( $revTwo, $secondReturn );
+
+ $firstMainSlot = $firstReturn->getSlot( SlotRecord::MAIN );
+ $secondMainSlot = $secondReturn->getSlot( SlotRecord::MAIN );
+
+ $this->assertSameSlotContent( $firstMainSlot, $secondMainSlot );
+
+ // And that different revisions have been created.
+ $this->assertNotSame( $firstReturn->getId(), $secondReturn->getId() );
+
+ // Make sure the slot rows reference the correct revision
+ $this->assertSame( $firstReturn->getId(), $firstMainSlot->getRevision() );
+ $this->assertSame( $secondReturn->getId(), $secondMainSlot->getRevision() );
+ }
+
+ public function provideInsertRevisionOn_failures() {
+ yield 'no slot' => [
+ [
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ new InvalidArgumentException( 'main slot must be provided' )
+ ];
+ yield 'no main slot' => [
+ [
+ 'slot' => SlotRecord::newUnsaved( 'aux', new WikitextContent( 'Turkey' ) ),
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ new InvalidArgumentException( 'main slot must be provided' )
+ ];
+ yield 'no timestamp' => [
+ [
+ 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'user' => true,
+ ],
+ new IncompleteRevisionException( 'timestamp field must not be NULL!' )
+ ];
+ yield 'no comment' => [
+ [
+ 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+ 'timestamp' => '20171117010101',
+ 'user' => true,
+ ],
+ new IncompleteRevisionException( 'comment must not be NULL!' )
+ ];
+ yield 'no user' => [
+ [
+ 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
+ 'comment' => $this->getRandomCommentStoreComment(),
+ 'timestamp' => '20171117010101',
+ ],
+ new IncompleteRevisionException( 'user must not be NULL!' )
+ ];
+ }
+
+ /**
+ * @dataProvider provideInsertRevisionOn_failures
+ * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+ */
+ public function testInsertRevisionOn_failures(
+ array $revDetails = [],
+ Exception $exception
+ ) {
+ $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $this->setExpectedException(
+ get_class( $exception ),
+ $exception->getMessage(),
+ $exception->getCode()
+ );
+ $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
+ }
+
+ public function provideNewNullRevision() {
+ yield [
+ Title::newFromText( 'UTPage_notAutoCreated' ),
+ [ 'content' => [ 'main' => new WikitextContent( 'Flubber1' ) ] ],
+ CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ),
+ true,
+ ];
+ yield [
+ Title::newFromText( 'UTPage_notAutoCreated' ),
+ [ 'content' => [ 'main' => new WikitextContent( 'Flubber2' ) ] ],
+ CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ),
+ false,
+ ];
+ }
+
+ /**
+ * @dataProvider provideNewNullRevision
+ * @covers \MediaWiki\Revision\RevisionStore::newNullRevision
+ * @covers \MediaWiki\Revision\RevisionStore::findSlotContentId
+ */
+ public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
+ $this->overrideMwServices();
+
+ $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
+ $page = WikiPage::factory( $title );
+
+ if ( !$page->exists() ) {
+ $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__, EDIT_NEW );
+ }
+
+ $revDetails['page'] = $page->getId();
+ $revDetails['timestamp'] = wfTimestampNow();
+ $revDetails['comment'] = CommentStoreComment::newUnsavedComment( 'Base' );
+ $revDetails['user'] = $user;
+
+ $baseRev = $this->getRevisionRecordFromDetailsArray( $revDetails );
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $baseRev = $store->insertRevisionOn( $baseRev, $dbw );
+ $page->updateRevisionOn( $dbw, new Revision( $baseRev ), $page->getLatest() );
+
+ $record = $store->newNullRevision(
+ wfGetDB( DB_MASTER ),
+ $title,
+ $comment,
+ $minor,
+ $user
+ );
+
+ $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() );
+ $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() );
+ $this->assertEquals( $comment, $record->getComment() );
+ $this->assertEquals( $minor, $record->isMinor() );
+ $this->assertEquals( $user->getName(), $record->getUser()->getName() );
+ $this->assertEquals( $baseRev->getId(), $record->getParentId() );
+
+ $this->assertArrayEquals(
+ $baseRev->getSlotRoles(),
+ $record->getSlotRoles()
+ );
+
+ foreach ( $baseRev->getSlotRoles() as $role ) {
+ $parentSlot = $baseRev->getSlot( $role );
+ $slot = $record->getSlot( $role );
+
+ $this->assertTrue( $slot->isInherited(), 'isInherited' );
+ $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
+ $this->assertSameSlotContent( $parentSlot, $slot );
+ }
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newNullRevision
+ */
+ public function testNewNullRevision_nonExistingTitle() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $record = $store->newNullRevision(
+ wfGetDB( DB_MASTER ),
+ Title::newFromText( __METHOD__ . '.iDontExist!' ),
+ CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ),
+ false,
+ TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser()
+ );
+ $this->assertNull( $record );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getRcIdIfUnpatrolled
+ */
+ public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() {
+ $page = $this->getTestPage();
+ $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+ /** @var Revision $rev */
+ $rev = $status->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revisionRecord = $store->getRevisionById( $rev->getId() );
+ $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
+
+ $this->assertGreaterThan( 0, $result );
+ $this->assertSame(
+ $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
+ $result
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getRcIdIfUnpatrolled
+ */
+ public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() {
+ // This assumes that sysops are auto patrolled
+ $sysop = $this->getTestSysop()->getUser();
+ $page = $this->getTestPage();
+ $status = $page->doEditContent(
+ new WikitextContent( __METHOD__ ),
+ __METHOD__,
+ 0,
+ false,
+ $sysop
+ );
+ /** @var Revision $rev */
+ $rev = $status->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revisionRecord = $store->getRevisionById( $rev->getId() );
+ $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
+
+ $this->assertSame( 0, $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getRecentChange
+ */
+ public function testGetRecentChange() {
+ $page = $this->getTestPage();
+ $content = new WikitextContent( __METHOD__ );
+ $status = $page->doEditContent( $content, __METHOD__ );
+ /** @var Revision $rev */
+ $rev = $status->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revRecord = $store->getRevisionById( $rev->getId() );
+ $recentChange = $store->getRecentChange( $revRecord );
+
+ $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
+ $this->assertEquals( $rev->getRecentChange(), $recentChange );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getRevisionById
+ */
+ public function testGetRevisionById() {
+ $page = $this->getTestPage();
+ $content = new WikitextContent( __METHOD__ );
+ $status = $page->doEditContent( $content, __METHOD__ );
+ /** @var Revision $rev */
+ $rev = $status->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revRecord = $store->getRevisionById( $rev->getId() );
+
+ $this->assertSame( $rev->getId(), $revRecord->getId() );
+ $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+ $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getRevisionByTitle
+ */
+ public function testGetRevisionByTitle() {
+ $page = $this->getTestPage();
+ $content = new WikitextContent( __METHOD__ );
+ $status = $page->doEditContent( $content, __METHOD__ );
+ /** @var Revision $rev */
+ $rev = $status->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revRecord = $store->getRevisionByTitle( $page->getTitle() );
+
+ $this->assertSame( $rev->getId(), $revRecord->getId() );
+ $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+ $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getRevisionByPageId
+ */
+ public function testGetRevisionByPageId() {
+ $page = $this->getTestPage();
+ $content = new WikitextContent( __METHOD__ );
+ $status = $page->doEditContent( $content, __METHOD__ );
+ /** @var Revision $rev */
+ $rev = $status->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revRecord = $store->getRevisionByPageId( $page->getId() );
+
+ $this->assertSame( $rev->getId(), $revRecord->getId() );
+ $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+ $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getRevisionByTimestamp
+ */
+ public function testGetRevisionByTimestamp() {
+ // Make sure there is 1 second between the last revision and the rev we create...
+ // Otherwise we might not get the correct revision and the test may fail...
+ // :(
+ $page = $this->getTestPage();
+ sleep( 1 );
+ $content = new WikitextContent( __METHOD__ );
+ $status = $page->doEditContent( $content, __METHOD__ );
+ /** @var Revision $rev */
+ $rev = $status->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $revRecord = $store->getRevisionByTimestamp(
+ $page->getTitle(),
+ $rev->getTimestamp()
+ );
+
+ $this->assertSame( $rev->getId(), $revRecord->getId() );
+ $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
+ $this->assertSame( __METHOD__, $revRecord->getComment()->text );
+ }
+
+ protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+ // XXX: the WikiPage object loads another RevisionRecord from the database. Not great.
+ $page = WikiPage::factory( $rev->getTitle() );
+
+ $fields = [
+ 'rev_id' => (string)$rev->getId(),
+ 'rev_page' => (string)$rev->getPage(),
+ 'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
+ 'rev_user_text' => (string)$rev->getUserText(),
+ 'rev_user' => (string)$rev->getUser(),
+ 'rev_minor_edit' => $rev->isMinor() ? '1' : '0',
+ 'rev_deleted' => (string)$rev->getVisibility(),
+ 'rev_len' => (string)$rev->getSize(),
+ 'rev_parent_id' => (string)$rev->getParentId(),
+ 'rev_sha1' => (string)$rev->getSha1(),
+ ];
+
+ if ( in_array( 'page', $options ) ) {
+ $fields += [
+ 'page_namespace' => (string)$page->getTitle()->getNamespace(),
+ 'page_title' => $page->getTitle()->getDBkey(),
+ 'page_id' => (string)$page->getId(),
+ 'page_latest' => (string)$page->getLatest(),
+ 'page_is_redirect' => $page->isRedirect() ? '1' : '0',
+ 'page_len' => (string)$page->getContent()->getSize(),
+ ];
+ }
+
+ if ( in_array( 'user', $options ) ) {
+ $fields += [
+ 'user_name' => (string)$rev->getUserText(),
+ ];
+ }
+
+ if ( in_array( 'comment', $options ) ) {
+ $fields += [
+ 'rev_comment_text' => $rev->getComment(),
+ 'rev_comment_data' => null,
+ 'rev_comment_cid' => null,
+ ];
+ }
+
+ if ( $rev->getId() ) {
+ $fields += [
+ 'rev_id' => (string)$rev->getId(),
+ ];
+ }
+
+ return (object)$fields;
+ }
+
+ protected function assertRevisionRecordMatchesRevision(
+ Revision $rev,
+ RevisionRecord $record
+ ) {
+ $this->assertSame( $rev->getId(), $record->getId() );
+ $this->assertSame( $rev->getPage(), $record->getPageId() );
+ $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() );
+ $this->assertSame( $rev->getUserText(), $record->getUser()->getName() );
+ $this->assertSame( $rev->getUser(), $record->getUser()->getId() );
+ $this->assertSame( $rev->isMinor(), $record->isMinor() );
+ $this->assertSame( $rev->getVisibility(), $record->getVisibility() );
+ $this->assertSame( $rev->getSize(), $record->getSize() );
+ /**
+ * @note As of MW 1.31, the database schema allows the parent ID to be
+ * NULL to indicate that it is unknown.
+ */
+ $expectedParent = $rev->getParentId();
+ if ( $expectedParent === null ) {
+ $expectedParent = 0;
+ }
+ $this->assertSame( $expectedParent, $record->getParentId() );
+ $this->assertSame( $rev->getSha1(), $record->getSha1() );
+ $this->assertSame( $rev->getComment(), $record->getComment()->text );
+ $this->assertSame( $rev->getContentFormat(),
+ $record->getContent( SlotRecord::MAIN )->getDefaultFormat() );
+ $this->assertSame( $rev->getContentModel(), $record->getContent( SlotRecord::MAIN )->getModel() );
+ $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() );
+
+ $revRec = $rev->getRevisionRecord();
+ $revMain = $revRec->getSlot( SlotRecord::MAIN );
+ $recMain = $record->getSlot( SlotRecord::MAIN );
+
+ $this->assertSame( $revMain->hasOrigin(), $recMain->hasOrigin(), 'hasOrigin' );
+ $this->assertSame( $revMain->hasAddress(), $recMain->hasAddress(), 'hasAddress' );
+ $this->assertSame( $revMain->hasContentId(), $recMain->hasContentId(), 'hasContentId' );
+
+ if ( $revMain->hasOrigin() ) {
+ $this->assertSame( $revMain->getOrigin(), $recMain->getOrigin(), 'getOrigin' );
+ }
+
+ if ( $revMain->hasAddress() ) {
+ $this->assertSame( $revMain->getAddress(), $recMain->getAddress(), 'getAddress' );
+ }
+
+ if ( $revMain->hasContentId() ) {
+ $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' );
+ }
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ * @covers \MediaWiki\Revision\RevisionStore::getQueryInfo
+ */
+ public function testNewRevisionFromRow_getQueryInfo() {
+ $page = $this->getTestPage();
+ $text = __METHOD__ . 'a-ä';
+ /** @var Revision $rev */
+ $rev = $page->doEditContent(
+ new WikitextContent( $text ),
+ __METHOD__ . 'a'
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $info = $store->getQueryInfo();
+ $row = $this->db->selectRow(
+ $info['tables'],
+ $info['fields'],
+ [ 'rev_id' => $rev->getId() ],
+ __METHOD__,
+ [],
+ $info['joins']
+ );
+ $record = $store->newRevisionFromRow(
+ $row,
+ [],
+ $page->getTitle()
+ );
+ $this->assertRevisionRecordMatchesRevision( $rev, $record );
+ $this->assertSame( $text, $rev->getContent()->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ */
+ public function testNewRevisionFromRow_anonEdit() {
+ $page = $this->getTestPage();
+ $text = __METHOD__ . 'a-ä';
+ /** @var Revision $rev */
+ $rev = $page->doEditContent(
+ new WikitextContent( $text ),
+ __METHOD__ . 'a'
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $record = $store->newRevisionFromRow(
+ $this->revisionToRow( $rev ),
+ [],
+ $page->getTitle()
+ );
+ $this->assertRevisionRecordMatchesRevision( $rev, $record );
+ $this->assertSame( $text, $rev->getContent()->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ */
+ public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
+ $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
+ $this->overrideMwServices();
+ $page = $this->getTestPage();
+ $text = __METHOD__ . 'a-ä';
+ /** @var Revision $rev */
+ $rev = $page->doEditContent(
+ new WikitextContent( $text ),
+ __METHOD__ . 'a'
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $record = $store->newRevisionFromRow(
+ $this->revisionToRow( $rev ),
+ [],
+ $page->getTitle()
+ );
+ $this->assertRevisionRecordMatchesRevision( $rev, $record );
+ $this->assertSame( $text, $rev->getContent()->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ */
+ public function testNewRevisionFromRow_userEdit() {
+ $page = $this->getTestPage();
+ $text = __METHOD__ . 'b-ä';
+ /** @var Revision $rev */
+ $rev = $page->doEditContent(
+ new WikitextContent( $text ),
+ __METHOD__ . 'b',
+ 0,
+ false,
+ $this->getTestUser()->getUser()
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $record = $store->newRevisionFromRow(
+ $this->revisionToRow( $rev ),
+ [],
+ $page->getTitle()
+ );
+ $this->assertRevisionRecordMatchesRevision( $rev, $record );
+ $this->assertSame( $text, $rev->getContent()->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+ * @covers \MediaWiki\Revision\RevisionStore::getArchiveQueryInfo
+ */
+ public function testNewRevisionFromArchiveRow_getArchiveQueryInfo() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $title = Title::newFromText( __METHOD__ );
+ $text = __METHOD__ . '-bä';
+ $page = WikiPage::factory( $title );
+ /** @var Revision $orig */
+ $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
+ ->value['revision'];
+ $page->doDeleteArticle( __METHOD__ );
+
+ $db = wfGetDB( DB_MASTER );
+ $arQuery = $store->getArchiveQueryInfo();
+ $res = $db->select(
+ $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+ __METHOD__, [], $arQuery['joins']
+ );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+ $record = $store->newRevisionFromArchiveRow( $row );
+
+ $this->assertRevisionRecordMatchesRevision( $orig, $record );
+ $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+ */
+ public function testNewRevisionFromArchiveRow_legacyEncoding() {
+ $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
+ $this->overrideMwServices();
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $title = Title::newFromText( __METHOD__ );
+ $text = __METHOD__ . '-bä';
+ $page = WikiPage::factory( $title );
+ /** @var Revision $orig */
+ $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
+ ->value['revision'];
+ $page->doDeleteArticle( __METHOD__ );
+
+ $db = wfGetDB( DB_MASTER );
+ $arQuery = $store->getArchiveQueryInfo();
+ $res = $db->select(
+ $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+ __METHOD__, [], $arQuery['joins']
+ );
+ $this->assertTrue( is_object( $res ), 'query failed' );
+
+ $row = $res->fetchObject();
+ $res->free();
+ $record = $store->newRevisionFromArchiveRow( $row );
+
+ $this->assertRevisionRecordMatchesRevision( $orig, $record );
+ $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromArchiveRow
+ */
+ public function testNewRevisionFromArchiveRow_no_user() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $row = (object)[
+ 'ar_id' => '1',
+ 'ar_page_id' => '2',
+ 'ar_namespace' => '0',
+ 'ar_title' => 'Something',
+ 'ar_rev_id' => '2',
+ 'ar_text_id' => '47',
+ 'ar_timestamp' => '20180528192356',
+ 'ar_minor_edit' => '0',
+ 'ar_deleted' => '0',
+ 'ar_len' => '78',
+ 'ar_parent_id' => '0',
+ 'ar_sha1' => 'deadbeef',
+ 'ar_comment_text' => 'whatever',
+ 'ar_comment_data' => null,
+ 'ar_comment_cid' => null,
+ 'ar_user' => '0',
+ 'ar_user_text' => '', // this is the important bit
+ 'ar_actor' => null,
+ 'ar_content_format' => null,
+ 'ar_content_model' => null,
+ ];
+
+ \Wikimedia\suppressWarnings();
+ $record = $store->newRevisionFromArchiveRow( $row );
+ \Wikimedia\suppressWarnings( true );
+
+ $this->assertInstanceOf( RevisionRecord::class, $record );
+ $this->assertInstanceOf( UserIdentityValue::class, $record->getUser() );
+ $this->assertSame( 'Unknown user', $record->getUser()->getName() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ */
+ public function testNewRevisionFromRow_no_user() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $title = Title::newFromText( __METHOD__ );
+
+ $row = (object)[
+ 'rev_id' => '2',
+ 'rev_page' => '2',
+ 'page_namespace' => '0',
+ 'page_title' => $title->getText(),
+ 'rev_text_id' => '47',
+ 'rev_timestamp' => '20180528192356',
+ 'rev_minor_edit' => '0',
+ 'rev_deleted' => '0',
+ 'rev_len' => '78',
+ 'rev_parent_id' => '0',
+ 'rev_sha1' => 'deadbeef',
+ 'rev_comment_text' => 'whatever',
+ 'rev_comment_data' => null,
+ 'rev_comment_cid' => null,
+ 'rev_user' => '0',
+ 'rev_user_text' => '', // this is the important bit
+ 'rev_actor' => null,
+ 'rev_content_format' => null,
+ 'rev_content_model' => null,
+ ];
+
+ \Wikimedia\suppressWarnings();
+ $record = $store->newRevisionFromRow( $row, 0, $title );
+ \Wikimedia\suppressWarnings( true );
+
+ $this->assertNotNull( $record );
+ $this->assertNotNull( $record->getUser() );
+ $this->assertNotEmpty( $record->getUser()->getName() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::insertRevisionOn
+ */
+ public function testInsertRevisionOn_archive() {
+ // This is a round trip test for deletion and undeletion of a
+ // revision row via the archive table.
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $title = Title::newFromText( __METHOD__ );
+
+ $page = WikiPage::factory( $title );
+ /** @var Revision $origRev */
+ $page->doEditContent( new WikitextContent( "First" ), __METHOD__ . '-first' );
+ $origRev = $page->doEditContent( new WikitextContent( "Foo" ), __METHOD__ )
+ ->value['revision'];
+ $orig = $origRev->getRevisionRecord();
+ $page->doDeleteArticle( __METHOD__ );
+
+ // re-create page, so we can later load revisions for it
+ $page->doEditContent( new WikitextContent( 'Two' ), __METHOD__ );
+
+ $db = wfGetDB( DB_MASTER );
+ $arQuery = $store->getArchiveQueryInfo();
+ $row = $db->selectRow(
+ $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+ __METHOD__, [], $arQuery['joins']
+ );
+
+ $this->assertNotFalse( $row, 'query failed' );
+
+ $record = $store->newRevisionFromArchiveRow(
+ $row,
+ 0,
+ $title,
+ [ 'page_id' => $title->getArticleID() ]
+ );
+
+ $restored = $store->insertRevisionOn( $record, $db );
+
+ // is the new revision correct?
+ $this->assertRevisionCompleteness( $restored );
+ $this->assertRevisionRecordsEqual( $record, $restored );
+
+ // does the new revision use the original slot?
+ $recMain = $record->getSlot( SlotRecord::MAIN );
+ $restMain = $restored->getSlot( SlotRecord::MAIN );
+ $this->assertSame( $recMain->getAddress(), $restMain->getAddress() );
+ $this->assertSame( $recMain->getContentId(), $restMain->getContentId() );
+ $this->assertSame( $recMain->getOrigin(), $restMain->getOrigin() );
+ $this->assertSame( 'Foo', $restMain->getContent()->serialize() );
+
+ // can we load it from the store?
+ $loaded = $store->getRevisionById( $restored->getId() );
+ $this->assertNotNull( $loaded );
+ $this->assertRevisionCompleteness( $loaded );
+ $this->assertRevisionRecordsEqual( $restored, $loaded );
+
+ // can we find it directly in the database?
+ $this->assertRevisionExistsInDatabase( $restored );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromId
+ */
+ public function testLoadRevisionFromId() {
+ $title = Title::newFromText( __METHOD__ );
+ $page = WikiPage::factory( $title );
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() );
+ $this->assertRevisionRecordMatchesRevision( $rev, $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromPageId
+ */
+ public function testLoadRevisionFromPageId() {
+ $title = Title::newFromText( __METHOD__ );
+ $page = WikiPage::factory( $title );
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() );
+ $this->assertRevisionRecordMatchesRevision( $rev, $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromTitle
+ */
+ public function testLoadRevisionFromTitle() {
+ $title = Title::newFromText( __METHOD__ );
+ $page = WikiPage::factory( $title );
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title );
+ $this->assertRevisionRecordMatchesRevision( $rev, $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::loadRevisionFromTimestamp
+ */
+ public function testLoadRevisionFromTimestamp() {
+ $title = Title::newFromText( __METHOD__ );
+ $page = WikiPage::factory( $title );
+ /** @var Revision $revOne */
+ $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
+ // Sleep to ensure different timestamps... )(evil)
+ sleep( 1 );
+ /** @var Revision $revTwo */
+ $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' )
+ ->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $this->assertNull(
+ $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' )
+ );
+ $this->assertSame(
+ $revOne->getId(),
+ $store->loadRevisionFromTimestamp(
+ wfGetDB( DB_MASTER ),
+ $title,
+ $revOne->getTimestamp()
+ )->getId()
+ );
+ $this->assertSame(
+ $revTwo->getId(),
+ $store->loadRevisionFromTimestamp(
+ wfGetDB( DB_MASTER ),
+ $title,
+ $revTwo->getTimestamp()
+ )->getId()
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::listRevisionSizes
+ */
+ public function testGetParentLengths() {
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+ /** @var Revision $revOne */
+ $revOne = $page->doEditContent(
+ new WikitextContent( __METHOD__ ), __METHOD__
+ )->value['revision'];
+ /** @var Revision $revTwo */
+ $revTwo = $page->doEditContent(
+ new WikitextContent( __METHOD__ . '2' ), __METHOD__
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $this->assertSame(
+ [
+ $revOne->getId() => strlen( __METHOD__ ),
+ ],
+ $store->listRevisionSizes(
+ wfGetDB( DB_MASTER ),
+ [ $revOne->getId() ]
+ )
+ );
+ $this->assertSame(
+ [
+ $revOne->getId() => strlen( __METHOD__ ),
+ $revTwo->getId() => strlen( __METHOD__ ) + 1,
+ ],
+ $store->listRevisionSizes(
+ wfGetDB( DB_MASTER ),
+ [ $revOne->getId(), $revTwo->getId() ]
+ )
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getPreviousRevision
+ */
+ public function testGetPreviousRevision() {
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+ /** @var Revision $revOne */
+ $revOne = $page->doEditContent(
+ new WikitextContent( __METHOD__ ), __METHOD__
+ )->value['revision'];
+ /** @var Revision $revTwo */
+ $revTwo = $page->doEditContent(
+ new WikitextContent( __METHOD__ . '2' ), __METHOD__
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $this->assertNull(
+ $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) )
+ );
+ $this->assertSame(
+ $revOne->getId(),
+ $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId()
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getNextRevision
+ */
+ public function testGetNextRevision() {
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+ /** @var Revision $revOne */
+ $revOne = $page->doEditContent(
+ new WikitextContent( __METHOD__ ), __METHOD__
+ )->value['revision'];
+ /** @var Revision $revTwo */
+ $revTwo = $page->doEditContent(
+ new WikitextContent( __METHOD__ . '2' ), __METHOD__
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $this->assertSame(
+ $revTwo->getId(),
+ $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId()
+ );
+ $this->assertNull(
+ $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) )
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getTimestampFromId
+ */
+ public function testGetTimestampFromId_found() {
+ $page = $this->getTestPage();
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->getTimestampFromId(
+ $page->getTitle(),
+ $rev->getId()
+ );
+
+ $this->assertSame( $rev->getTimestamp(), $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getTimestampFromId
+ */
+ public function testGetTimestampFromId_notFound() {
+ $page = $this->getTestPage();
+ /** @var Revision $rev */
+ $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
+ ->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->getTimestampFromId(
+ $page->getTitle(),
+ $rev->getId() + 1
+ );
+
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::countRevisionsByPageId
+ */
+ public function testCountRevisionsByPageId() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+
+ $this->assertSame(
+ 0,
+ $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
+ );
+ $page->doEditContent( new WikitextContent( 'a' ), 'a' );
+ $this->assertSame(
+ 1,
+ $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
+ );
+ $page->doEditContent( new WikitextContent( 'b' ), 'b' );
+ $this->assertSame(
+ 2,
+ $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::countRevisionsByTitle
+ */
+ public function testCountRevisionsByTitle() {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
+
+ $this->assertSame(
+ 0,
+ $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
+ );
+ $page->doEditContent( new WikitextContent( 'a' ), 'a' );
+ $this->assertSame(
+ 1,
+ $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
+ );
+ $page->doEditContent( new WikitextContent( 'b' ), 'b' );
+ $this->assertSame(
+ 2,
+ $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::userWasLastToEdit
+ */
+ public function testUserWasLastToEdit_false() {
+ $sysop = $this->getTestSysop()->getUser();
+ $page = $this->getTestPage();
+ $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->userWasLastToEdit(
+ wfGetDB( DB_MASTER ),
+ $page->getId(),
+ $sysop->getId(),
+ '20160101010101'
+ );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::userWasLastToEdit
+ */
+ public function testUserWasLastToEdit_true() {
+ $startTime = wfTimestampNow();
+ $sysop = $this->getTestSysop()->getUser();
+ $page = $this->getTestPage();
+ $page->doEditContent(
+ new WikitextContent( __METHOD__ ),
+ __METHOD__,
+ 0,
+ false,
+ $sysop
+ );
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $result = $store->userWasLastToEdit(
+ wfGetDB( DB_MASTER ),
+ $page->getId(),
+ $sysop->getId(),
+ $startTime
+ );
+ $this->assertTrue( $result );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getKnownCurrentRevision
+ */
+ public function testGetKnownCurrentRevision() {
+ $page = $this->getTestPage();
+ /** @var Revision $rev */
+ $rev = $page->doEditContent(
+ new WikitextContent( __METHOD__ . 'b' ),
+ __METHOD__ . 'b',
+ 0,
+ false,
+ $this->getTestUser()->getUser()
+ )->value['revision'];
+
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+ $record = $store->getKnownCurrentRevision(
+ $page->getTitle(),
+ $rev->getId()
+ );
+
+ $this->assertRevisionRecordMatchesRevision( $rev, $record );
+ }
+
+ public function provideNewMutableRevisionFromArray() {
+ yield 'Basic array, content object' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'content' => new WikitextContent( 'Some Content' ),
+ ]
+ ];
+ yield 'Basic array, serialized text' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
+ ]
+ ];
+ yield 'Basic array, serialized text, utf-8 flags' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
+ 'flags' => 'utf-8',
+ ]
+ ];
+ yield 'Basic array, with title' => [
+ [
+ 'title' => Title::newFromText( 'SomeText' ),
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.2',
+ 'user' => 0,
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'content' => new WikitextContent( 'Some Content' ),
+ ]
+ ];
+ yield 'Basic array, no user field' => [
+ [
+ 'id' => 2,
+ 'page' => 1,
+ 'timestamp' => '20171017114835',
+ 'user_text' => '111.0.1.3',
+ 'minor_edit' => false,
+ 'deleted' => 0,
+ 'len' => 46,
+ 'parent_id' => 1,
+ 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+ 'comment' => 'Goat Comment!',
+ 'content' => new WikitextContent( 'Some Content' ),
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider provideNewMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
+ */
+ public function testNewMutableRevisionFromArray( array $array ) {
+ $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+ // HACK: if $array['page'] is given, make sure that the page exists
+ if ( isset( $array['page'] ) ) {
+ $t = Title::newFromID( $array['page'] );
+ if ( !$t || !$t->exists() ) {
+ $t = Title::makeTitle( NS_MAIN, __METHOD__ );
+ $info = $this->insertPage( $t );
+ $array['page'] = $info['id'];
+ }
+ }
+
+ $result = $store->newMutableRevisionFromArray( $array );
+
+ if ( isset( $array['id'] ) ) {
+ $this->assertSame( $array['id'], $result->getId() );
+ }
+ if ( isset( $array['page'] ) ) {
+ $this->assertSame( $array['page'], $result->getPageId() );
+ }
+ $this->assertSame( $array['timestamp'], $result->getTimestamp() );
+ $this->assertSame( $array['user_text'], $result->getUser()->getName() );
+ if ( isset( $array['user'] ) ) {
+ $this->assertSame( $array['user'], $result->getUser()->getId() );
+ }
+ $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() );
+ $this->assertSame( $array['deleted'], $result->getVisibility() );
+ $this->assertSame( $array['len'], $result->getSize() );
+ $this->assertSame( $array['parent_id'], $result->getParentId() );
+ $this->assertSame( $array['sha1'], $result->getSha1() );
+ $this->assertSame( $array['comment'], $result->getComment()->text );
+ if ( isset( $array['content'] ) ) {
+ foreach ( $array['content'] as $role => $content ) {
+ $this->assertTrue(
+ $result->getContent( $role )->equals( $content )
+ );
+ }
+ } elseif ( isset( $array['text'] ) ) {
+ $this->assertSame( $array['text'],
+ $result->getSlot( SlotRecord::MAIN )->getContent()->serialize() );
+ } elseif ( isset( $array['content_format'] ) ) {
+ $this->assertSame(
+ $array['content_format'],
+ $result->getSlot( SlotRecord::MAIN )->getContent()->getDefaultFormat()
+ );
+ $this->assertSame( $array['content_model'], $result->getSlot( SlotRecord::MAIN )->getModel() );
+ }
+ }
+
+ /**
+ * @dataProvider provideNewMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
+ */
+ public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) {
+ $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+ $blobStore = new SqlBlobStore( $lb, $cache );
+ $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
+
+ $factory = $this->getMockBuilder( BlobStoreFactory::class )
+ ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $factory->expects( $this->any() )
+ ->method( 'newBlobStore' )
+ ->willReturn( $blobStore );
+ $factory->expects( $this->any() )
+ ->method( 'newSqlBlobStore' )
+ ->willReturn( $blobStore );
+
+ $this->setService( 'BlobStoreFactory', $factory );
+
+ $this->testNewMutableRevisionFromArray( $array );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use ActorMigration;
+use CommentStore;
+use MediaWiki\Logger\Spi as LoggerSpi;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\RevisionStoreFactory;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWikiTestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use WANObjectCache;
+use Wikimedia\Rdbms\ILBFactory;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+
+class RevisionStoreFactoryTest extends MediaWikiTestCase {
+
+ public function testValidConstruction_doesntCauseErrors() {
+ new RevisionStoreFactory(
+ $this->getMockLoadBalancerFactory(),
+ $this->getMockBlobStoreFactory(),
+ $this->getNameTableStoreFactory(),
+ $this->getHashWANObjectCache(),
+ $this->getMockCommentStore(),
+ ActorMigration::newMigration(),
+ MIGRATION_OLD,
+ $this->getMockLoggerSpi(),
+ true
+ );
+ $this->assertTrue( true );
+ }
+
+ public function provideWikiIds() {
+ yield [ true ];
+ yield [ false ];
+ yield [ 'somewiki' ];
+ yield [ 'somewiki', MIGRATION_OLD , false ];
+ yield [ 'somewiki', MIGRATION_NEW , true ];
+ }
+
+ /**
+ * @dataProvider provideWikiIds
+ */
+ public function testGetRevisionStore(
+ $wikiId,
+ $mcrMigrationStage = MIGRATION_OLD,
+ $contentHandlerUseDb = true
+ ) {
+ $lbFactory = $this->getMockLoadBalancerFactory();
+ $blobStoreFactory = $this->getMockBlobStoreFactory();
+ $nameTableStoreFactory = $this->getNameTableStoreFactory();
+ $cache = $this->getHashWANObjectCache();
+ $commentStore = $this->getMockCommentStore();
+ $actorMigration = ActorMigration::newMigration();
+ $loggerProvider = $this->getMockLoggerSpi();
+
+ $factory = new RevisionStoreFactory(
+ $lbFactory,
+ $blobStoreFactory,
+ $nameTableStoreFactory,
+ $cache,
+ $commentStore,
+ $actorMigration,
+ $mcrMigrationStage,
+ $loggerProvider,
+ $contentHandlerUseDb
+ );
+
+ $store = $factory->getRevisionStore( $wikiId );
+ $wrapper = TestingAccessWrapper::newFromObject( $store );
+
+ // ensure the correct object type is returned
+ $this->assertInstanceOf( RevisionStore::class, $store );
+
+ // ensure the RevisionStore is for the given wikiId
+ $this->assertSame( $wikiId, $wrapper->wikiId );
+
+ // ensure all other required services are correctly set
+ $this->assertSame( $cache, $wrapper->cache );
+ $this->assertSame( $commentStore, $wrapper->commentStore );
+ $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
+ $this->assertSame( $actorMigration, $wrapper->actorMigration );
+ $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
+
+ $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
+ $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
+ $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
+ $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
+ $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
+ */
+ private function getMockLoadBalancer() {
+ return $this->getMockBuilder( ILoadBalancer::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
+ */
+ private function getMockLoadBalancerFactory() {
+ $mock = $this->getMockBuilder( ILBFactory::class )
+ ->disableOriginalConstructor()->getMock();
+
+ $mock->method( 'getMainLB' )
+ ->willReturnCallback( function () {
+ return $this->getMockLoadBalancer();
+ } );
+
+ return $mock;
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
+ */
+ private function getMockSqlBlobStore() {
+ return $this->getMockBuilder( SqlBlobStore::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
+ */
+ private function getMockBlobStoreFactory() {
+ $mock = $this->getMockBuilder( BlobStoreFactory::class )
+ ->disableOriginalConstructor()->getMock();
+
+ $mock->method( 'newSqlBlobStore' )
+ ->willReturnCallback( function () {
+ return $this->getMockSqlBlobStore();
+ } );
+
+ return $mock;
+ }
+
+ /**
+ * @return NameTableStoreFactory
+ */
+ private function getNameTableStoreFactory() {
+ return new NameTableStoreFactory(
+ $this->getMockLoadBalancerFactory(),
+ $this->getHashWANObjectCache(),
+ new NullLogger() );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
+ */
+ private function getMockCommentStore() {
+ return $this->getMockBuilder( CommentStore::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ private function getHashWANObjectCache() {
+ return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
+ */
+ private function getMockLoggerSpi() {
+ $mock = $this->getMock( LoggerSpi::class );
+
+ $mock->method( 'getLogger' )
+ ->willReturn( new NullLogger() );
+
+ return $mock;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStoreComment;
+use InvalidArgumentException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionStoreRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use MediaWikiTestCase;
+use TextContent;
+use Title;
+
+/**
+ * @covers \MediaWiki\Revision\RevisionStoreRecord
+ * @covers \MediaWiki\Revision\RevisionRecord
+ */
+class RevisionStoreRecordTest extends MediaWikiTestCase {
+
+ use RevisionRecordTests;
+
+ /**
+ * @param array $rowOverrides
+ *
+ * @return RevisionStoreRecord
+ */
+ protected function newRevision( array $rowOverrides = [] ) {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
+ $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+ $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+ $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+ $slots = new RevisionSlots( [ $main, $aux ] );
+
+ $row = [
+ 'rev_id' => '7',
+ 'rev_page' => strval( $title->getArticleID() ),
+ 'rev_timestamp' => '20200101000000',
+ 'rev_deleted' => 0,
+ 'rev_minor_edit' => 0,
+ 'rev_parent_id' => '5',
+ 'rev_len' => $slots->computeSize(),
+ 'rev_sha1' => $slots->computeSha1(),
+ 'page_latest' => '18',
+ ];
+
+ $row = array_merge( $row, $rowOverrides );
+
+ return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots );
+ }
+
+ public function provideConstructor() {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
+ $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+ $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+ $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+ $slots = new RevisionSlots( [ $main, $aux ] );
+
+ $protoRow = [
+ 'rev_id' => '7',
+ 'rev_page' => strval( $title->getArticleID() ),
+ 'rev_timestamp' => '20200101000000',
+ 'rev_deleted' => 0,
+ 'rev_minor_edit' => 0,
+ 'rev_parent_id' => '5',
+ 'rev_len' => $slots->computeSize(),
+ 'rev_sha1' => $slots->computeSha1(),
+ 'page_latest' => '18',
+ ];
+
+ $row = $protoRow;
+ yield 'all info' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots,
+ 'acmewiki'
+ ];
+
+ $row = $protoRow;
+ $row['rev_minor_edit'] = '1';
+ $row['rev_deleted'] = strval( RevisionRecord::DELETED_USER );
+
+ yield 'minor deleted' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ $row['page_latest'] = $row['rev_id'];
+
+ yield 'latest' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ unset( $row['rev_parent'] );
+
+ yield 'no parent' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ $row['rev_len'] = null;
+ $row['rev_sha1'] = '';
+
+ yield 'rev_len is null, rev_sha1 is ""' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ yield 'no length, no hash' => [
+ Title::newFromText( 'DummyDoesNotExist' ),
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+ }
+
+ /**
+ * @dataProvider provideConstructor
+ *
+ * @param Title $title
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row
+ * @param RevisionSlots $slots
+ * @param bool $wikiId
+ */
+ public function testConstructorAndGetters(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
+
+ $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
+ $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
+ $this->assertSame( $comment, $rec->getComment(), 'getComment' );
+
+ $this->assertSame( $slots, $rec->getSlots(), 'getSlots' );
+ $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
+ $this->assertSame( $slots->getSlots(), $rec->getSlots()->getSlots(), 'getSlots' );
+ $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
+
+ $this->assertSame( (int)$row->rev_id, $rec->getId(), 'getId' );
+ $this->assertSame( (int)$row->rev_page, $rec->getPageId(), 'getId' );
+ $this->assertSame( $row->rev_timestamp, $rec->getTimestamp(), 'getTimestamp' );
+ $this->assertSame( (int)$row->rev_deleted, $rec->getVisibility(), 'getVisibility' );
+ $this->assertSame( (bool)$row->rev_minor_edit, $rec->isMinor(), 'getIsMinor' );
+
+ if ( isset( $row->rev_parent_id ) ) {
+ $this->assertSame( (int)$row->rev_parent_id, $rec->getParentId(), 'getParentId' );
+ } else {
+ $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
+ }
+
+ if ( isset( $row->rev_len ) ) {
+ $this->assertSame( (int)$row->rev_len, $rec->getSize(), 'getSize' );
+ } else {
+ $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
+ }
+
+ if ( !empty( $row->rev_sha1 ) ) {
+ $this->assertSame( $row->rev_sha1, $rec->getSha1(), 'getSha1' );
+ } else {
+ $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
+ }
+
+ if ( isset( $row->page_latest ) ) {
+ $this->assertSame(
+ (int)$row->rev_id === (int)$row->page_latest,
+ $rec->isCurrent(),
+ 'isCurrent'
+ );
+ } else {
+ $this->assertSame(
+ false,
+ $rec->isCurrent(),
+ 'isCurrent'
+ );
+ }
+ }
+
+ public function provideConstructorFailure() {
+ $title = Title::newFromText( 'Dummy' );
+ $title->resetArticleID( 17 );
+
+ $user = new UserIdentityValue( 11, 'Tester', 0 );
+
+ $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
+
+ $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
+ $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
+ $slots = new RevisionSlots( [ $main, $aux ] );
+
+ $protoRow = [
+ 'rev_id' => '7',
+ 'rev_page' => strval( $title->getArticleID() ),
+ 'rev_timestamp' => '20200101000000',
+ 'rev_deleted' => 0,
+ 'rev_minor_edit' => 0,
+ 'rev_parent_id' => '5',
+ 'rev_len' => $slots->computeSize(),
+ 'rev_sha1' => $slots->computeSha1(),
+ 'page_latest' => '18',
+ ];
+
+ yield 'not a row' => [
+ $title,
+ $user,
+ $comment,
+ 'not a row',
+ $slots,
+ 'acmewiki'
+ ];
+
+ $row = $protoRow;
+ $row['rev_timestamp'] = 'kittens';
+
+ yield 'bad timestamp' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+ $row['rev_page'] = 99;
+
+ yield 'page ID mismatch' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots
+ ];
+
+ $row = $protoRow;
+
+ yield 'bad wiki' => [
+ $title,
+ $user,
+ $comment,
+ (object)$row,
+ $slots,
+ 12345
+ ];
+ }
+
+ /**
+ * @dataProvider provideConstructorFailure
+ *
+ * @param Title $title
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row
+ * @param RevisionSlots $slots
+ * @param bool $wikiId
+ */
+ public function testConstructorFailure(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+ new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
+ }
+
+ public function provideIsCurrent() {
+ yield [
+ [
+ 'rev_id' => 11,
+ 'page_latest' => 11,
+ ],
+ true,
+ ];
+ yield [
+ [
+ 'rev_id' => 11,
+ 'page_latest' => 10,
+ ],
+ false,
+ ];
+ }
+
+ /**
+ * @dataProvider provideIsCurrent
+ */
+ public function testIsCurrent( $row, $current ) {
+ $rev = $this->newRevision( $row );
+
+ $this->assertSame( $current, $rev->isCurrent(), 'isCurrent()' );
+ }
+
+ public function provideGetSlot_audience_latest() {
+ return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
+ }
+
+ /**
+ * @dataProvider provideGetSlot_audience_latest
+ */
+ public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) {
+ $this->forceStandardPermissions();
+
+ $user = $this->getTestUser( $groups )->getUser();
+ $rev = $this->newRevision(
+ [
+ 'rev_deleted' => $visibility,
+ 'rev_id' => 11,
+ 'page_latest' => 11, // revision is current
+ ]
+ );
+
+ // NOTE: slot meta-data is never suppressed, just the content is!
+ $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
+ $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
+ 'public can' );
+
+ $this->assertNotNull(
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
+ 'user can'
+ );
+
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getContent();
+ // NOTE: the content of the current revision is never suppressed!
+ // Check that getContent() doesn't throw SuppressedDataException
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
+ $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use CommentStore;
+use HashBagOStuff;
+use InvalidArgumentException;
+use Language;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWikiTestCase;
+use MWException;
+use Title;
+use WANObjectCache;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\TestingAccessWrapper;
+use WikitextContent;
+
+class RevisionStoreTest extends MediaWikiTestCase {
+
+ private function useTextId() {
+ global $wgMultiContentRevisionSchemaMigrationStage;
+
+ return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
+ }
+
+ /**
+ * @param LoadBalancer $loadBalancer
+ * @param SqlBlobStore $blobStore
+ * @param WANObjectCache $WANObjectCache
+ *
+ * @return RevisionStore
+ */
+ private function getRevisionStore(
+ $loadBalancer = null,
+ $blobStore = null,
+ $WANObjectCache = null
+ ) {
+ global $wgMultiContentRevisionSchemaMigrationStage;
+ // the migration stage should be irrelevant, since all the tests that interact with
+ // the database are in RevisionStoreDbTest, not here.
+
+ return new RevisionStore(
+ $loadBalancer ?: $this->getMockLoadBalancer(),
+ $blobStore ?: $this->getMockSqlBlobStore(),
+ $WANObjectCache ?: $this->getHashWANObjectCache(),
+ MediaWikiServices::getInstance()->getCommentStore(),
+ MediaWikiServices::getInstance()->getContentModelStore(),
+ MediaWikiServices::getInstance()->getSlotRoleStore(),
+ $wgMultiContentRevisionSchemaMigrationStage,
+ MediaWikiServices::getInstance()->getActorMigration()
+ );
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
+ */
+ private function getMockLoadBalancer() {
+ return $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|Database
+ */
+ private function getMockDatabase() {
+ return $this->getMockBuilder( Database::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
+ */
+ private function getMockSqlBlobStore() {
+ return $this->getMockBuilder( SqlBlobStore::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
+ */
+ private function getMockCommentStore() {
+ return $this->getMockBuilder( CommentStore::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ private function getHashWANObjectCache() {
+ return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
+ }
+
+ public function provideSetContentHandlerUseDB() {
+ return [
+ // ContentHandlerUseDB can be true of false pre migration.
+ [ false, SCHEMA_COMPAT_OLD, false ],
+ [ true, SCHEMA_COMPAT_OLD, false ],
+ // During and after migration it can not be false...
+ [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, true ],
+ [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, true ],
+ [ false, SCHEMA_COMPAT_NEW, true ],
+ // ...but it can be true.
+ [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
+ [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
+ [ true, SCHEMA_COMPAT_NEW, false ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSetContentHandlerUseDB
+ * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
+ * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
+ */
+ public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
+ if ( $expectedFail ) {
+ $this->setExpectedException( MWException::class );
+ }
+
+ $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
+
+ $store = new RevisionStore(
+ $this->getMockLoadBalancer(),
+ $this->getMockSqlBlobStore(),
+ $this->getHashWANObjectCache(),
+ $this->getMockCommentStore(),
+ $nameTables->getContentModels(),
+ $nameTables->getSlotRoles(),
+ $migrationMode,
+ MediaWikiServices::getInstance()->getActorMigration()
+ );
+
+ $store->setContentHandlerUseDB( $contentHandlerDb );
+ $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
+ }
+
+ public function testGetTitle_successFromPageId() {
+ $mockLoadBalancer = $this->getMockLoadBalancer();
+ // Title calls wfGetDB() so we have to set the main service
+ $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+ $db = $this->getMockDatabase();
+ // Title calls wfGetDB() which uses a regular Connection
+ $mockLoadBalancer->expects( $this->atLeastOnce() )
+ ->method( 'getConnection' )
+ ->willReturn( $db );
+
+ // First call to Title::newFromID, faking no result (db lag?)
+ $db->expects( $this->at( 0 ) )
+ ->method( 'selectRow' )
+ ->with(
+ 'page',
+ $this->anything(),
+ [ 'page_id' => 1 ]
+ )
+ ->willReturn( (object)[
+ 'page_namespace' => '1',
+ 'page_title' => 'Food',
+ ] );
+
+ $store = $this->getRevisionStore( $mockLoadBalancer );
+ $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+ $this->assertSame( 1, $title->getNamespace() );
+ $this->assertSame( 'Food', $title->getDBkey() );
+ }
+
+ public function testGetTitle_successFromPageIdOnFallback() {
+ $mockLoadBalancer = $this->getMockLoadBalancer();
+ // Title calls wfGetDB() so we have to set the main service
+ $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+ $db = $this->getMockDatabase();
+ // Title calls wfGetDB() which uses a regular Connection
+ // Assert that the first call uses a REPLICA and the second falls back to master
+ $mockLoadBalancer->expects( $this->exactly( 2 ) )
+ ->method( 'getConnection' )
+ ->willReturn( $db );
+ // RevisionStore getTitle uses a ConnectionRef
+ $mockLoadBalancer->expects( $this->atLeastOnce() )
+ ->method( 'getConnectionRef' )
+ ->willReturn( $db );
+
+ // First call to Title::newFromID, faking no result (db lag?)
+ $db->expects( $this->at( 0 ) )
+ ->method( 'selectRow' )
+ ->with(
+ 'page',
+ $this->anything(),
+ [ 'page_id' => 1 ]
+ )
+ ->willReturn( false );
+
+ // First select using rev_id, faking no result (db lag?)
+ $db->expects( $this->at( 1 ) )
+ ->method( 'selectRow' )
+ ->with(
+ [ 'revision', 'page' ],
+ $this->anything(),
+ [ 'rev_id' => 2 ]
+ )
+ ->willReturn( false );
+
+ // Second call to Title::newFromID, no result
+ $db->expects( $this->at( 2 ) )
+ ->method( 'selectRow' )
+ ->with(
+ 'page',
+ $this->anything(),
+ [ 'page_id' => 1 ]
+ )
+ ->willReturn( (object)[
+ 'page_namespace' => '2',
+ 'page_title' => 'Foodey',
+ ] );
+
+ $store = $this->getRevisionStore( $mockLoadBalancer );
+ $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+ $this->assertSame( 2, $title->getNamespace() );
+ $this->assertSame( 'Foodey', $title->getDBkey() );
+ }
+
+ public function testGetTitle_successFromRevId() {
+ $mockLoadBalancer = $this->getMockLoadBalancer();
+ // Title calls wfGetDB() so we have to set the main service
+ $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+ $db = $this->getMockDatabase();
+ // Title calls wfGetDB() which uses a regular Connection
+ $mockLoadBalancer->expects( $this->atLeastOnce() )
+ ->method( 'getConnection' )
+ ->willReturn( $db );
+ // RevisionStore getTitle uses a ConnectionRef
+ $mockLoadBalancer->expects( $this->atLeastOnce() )
+ ->method( 'getConnectionRef' )
+ ->willReturn( $db );
+
+ // First call to Title::newFromID, faking no result (db lag?)
+ $db->expects( $this->at( 0 ) )
+ ->method( 'selectRow' )
+ ->with(
+ 'page',
+ $this->anything(),
+ [ 'page_id' => 1 ]
+ )
+ ->willReturn( false );
+
+ // First select using rev_id, faking no result (db lag?)
+ $db->expects( $this->at( 1 ) )
+ ->method( 'selectRow' )
+ ->with(
+ [ 'revision', 'page' ],
+ $this->anything(),
+ [ 'rev_id' => 2 ]
+ )
+ ->willReturn( (object)[
+ 'page_namespace' => '1',
+ 'page_title' => 'Food2',
+ ] );
+
+ $store = $this->getRevisionStore( $mockLoadBalancer );
+ $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+ $this->assertSame( 1, $title->getNamespace() );
+ $this->assertSame( 'Food2', $title->getDBkey() );
+ }
+
+ public function testGetTitle_successFromRevIdOnFallback() {
+ $mockLoadBalancer = $this->getMockLoadBalancer();
+ // Title calls wfGetDB() so we have to set the main service
+ $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+ $db = $this->getMockDatabase();
+ // Title calls wfGetDB() which uses a regular Connection
+ // Assert that the first call uses a REPLICA and the second falls back to master
+ $mockLoadBalancer->expects( $this->exactly( 2 ) )
+ ->method( 'getConnection' )
+ ->willReturn( $db );
+ // RevisionStore getTitle uses a ConnectionRef
+ $mockLoadBalancer->expects( $this->atLeastOnce() )
+ ->method( 'getConnectionRef' )
+ ->willReturn( $db );
+
+ // First call to Title::newFromID, faking no result (db lag?)
+ $db->expects( $this->at( 0 ) )
+ ->method( 'selectRow' )
+ ->with(
+ 'page',
+ $this->anything(),
+ [ 'page_id' => 1 ]
+ )
+ ->willReturn( false );
+
+ // First select using rev_id, faking no result (db lag?)
+ $db->expects( $this->at( 1 ) )
+ ->method( 'selectRow' )
+ ->with(
+ [ 'revision', 'page' ],
+ $this->anything(),
+ [ 'rev_id' => 2 ]
+ )
+ ->willReturn( false );
+
+ // Second call to Title::newFromID, no result
+ $db->expects( $this->at( 2 ) )
+ ->method( 'selectRow' )
+ ->with(
+ 'page',
+ $this->anything(),
+ [ 'page_id' => 1 ]
+ )
+ ->willReturn( false );
+
+ // Second select using rev_id, result
+ $db->expects( $this->at( 3 ) )
+ ->method( 'selectRow' )
+ ->with(
+ [ 'revision', 'page' ],
+ $this->anything(),
+ [ 'rev_id' => 2 ]
+ )
+ ->willReturn( (object)[
+ 'page_namespace' => '2',
+ 'page_title' => 'Foodey',
+ ] );
+
+ $store = $this->getRevisionStore( $mockLoadBalancer );
+ $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+
+ $this->assertSame( 2, $title->getNamespace() );
+ $this->assertSame( 'Foodey', $title->getDBkey() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::getTitle
+ */
+ public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
+ $mockLoadBalancer = $this->getMockLoadBalancer();
+ // Title calls wfGetDB() so we have to set the main service
+ $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
+
+ $db = $this->getMockDatabase();
+ // Title calls wfGetDB() which uses a regular Connection
+ // Assert that the first call uses a REPLICA and the second falls back to master
+
+ // RevisionStore getTitle uses getConnectionRef
+ // Title::newFromID uses getConnection
+ foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
+ $mockLoadBalancer->expects( $this->exactly( 2 ) )
+ ->method( $method )
+ ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
+ static $callCounter = 0;
+ $callCounter++;
+ // The first call should be to a REPLICA, and the second a MASTER.
+ if ( $callCounter === 1 ) {
+ $this->assertSame( DB_REPLICA, $masterOrReplica );
+ } elseif ( $callCounter === 2 ) {
+ $this->assertSame( DB_MASTER, $masterOrReplica );
+ }
+ return $db;
+ } );
+ }
+ // First and third call to Title::newFromID, faking no result
+ foreach ( [ 0, 2 ] as $counter ) {
+ $db->expects( $this->at( $counter ) )
+ ->method( 'selectRow' )
+ ->with(
+ 'page',
+ $this->anything(),
+ [ 'page_id' => 1 ]
+ )
+ ->willReturn( false );
+ }
+
+ foreach ( [ 1, 3 ] as $counter ) {
+ $db->expects( $this->at( $counter ) )
+ ->method( 'selectRow' )
+ ->with(
+ [ 'revision', 'page' ],
+ $this->anything(),
+ [ 'rev_id' => 2 ]
+ )
+ ->willReturn( false );
+ }
+
+ $store = $this->getRevisionStore( $mockLoadBalancer );
+
+ $this->setExpectedException( RevisionAccessException::class );
+ $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
+ }
+
+ public function provideNewRevisionFromRow_legacyEncoding_applied() {
+ yield 'windows-1252, old_flags is empty' => [
+ 'windows-1252',
+ 'en',
+ [
+ 'old_flags' => '',
+ 'old_text' => "S\xF6me Content",
+ ],
+ 'Söme Content'
+ ];
+
+ yield 'windows-1252, old_flags is null' => [
+ 'windows-1252',
+ 'en',
+ [
+ 'old_flags' => null,
+ 'old_text' => "S\xF6me Content",
+ ],
+ 'Söme Content'
+ ];
+ }
+
+ /**
+ * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
+ *
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ */
+ public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
+ if ( !$this->useTextId() ) {
+ $this->markTestSkipped( 'No longer applicable with MCR schema' );
+ }
+
+ $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+
+ $blobStore = new SqlBlobStore( $lb, $cache );
+ $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
+
+ $store = $this->getRevisionStore( $lb, $blobStore, $cache );
+
+ $record = $store->newRevisionFromRow(
+ $this->makeRow( $row ),
+ 0,
+ Title::newFromText( __METHOD__ . '-UTPage' )
+ );
+
+ $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
+ */
+ public function testNewRevisionFromRow_legacyEncoding_ignored() {
+ if ( !$this->useTextId() ) {
+ $this->markTestSkipped( 'No longer applicable with MCR schema' );
+ }
+
+ $row = [
+ 'old_flags' => 'utf-8',
+ 'old_text' => 'Söme Content',
+ ];
+
+ $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
+ $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+
+ $blobStore = new SqlBlobStore( $lb, $cache );
+ $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
+
+ $store = $this->getRevisionStore( $lb, $blobStore, $cache );
+
+ $record = $store->newRevisionFromRow(
+ $this->makeRow( $row ),
+ 0,
+ Title::newFromText( __METHOD__ . '-UTPage' )
+ );
+ $this->assertSame( 'Söme Content', $record->getContent( SlotRecord::MAIN )->serialize() );
+ }
+
+ private function makeRow( array $array ) {
+ $row = $array + [
+ 'rev_id' => 7,
+ 'rev_page' => 5,
+ 'rev_timestamp' => '20110101000000',
+ 'rev_user_text' => 'Tester',
+ 'rev_user' => 17,
+ 'rev_minor_edit' => 0,
+ 'rev_deleted' => 0,
+ 'rev_len' => 100,
+ 'rev_parent_id' => 0,
+ 'rev_sha1' => 'deadbeef',
+ 'rev_comment_text' => 'Testing',
+ 'rev_comment_data' => '{}',
+ 'rev_comment_cid' => 111,
+ 'page_namespace' => 0,
+ 'page_title' => 'TEST',
+ 'page_id' => 5,
+ 'page_latest' => 7,
+ 'page_is_redirect' => 0,
+ 'page_len' => 100,
+ 'user_name' => 'Tester',
+ ];
+
+ if ( $this->useTextId() ) {
+ $row += [
+ 'rev_content_format' => CONTENT_FORMAT_TEXT,
+ 'rev_content_model' => CONTENT_MODEL_TEXT,
+ 'rev_text_id' => 11,
+ 'old_id' => 11,
+ 'old_text' => 'Hello World',
+ 'old_flags' => 'utf-8',
+ ];
+ } else {
+ if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
+ $row['content'] = [
+ 'main' => new WikitextContent( $array['old_text'] ),
+ ];
+ }
+ }
+
+ return (object)$row;
+ }
+
+ public function provideMigrationConstruction() {
+ return [
+ [ SCHEMA_COMPAT_OLD, false ],
+ [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
+ [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
+ [ SCHEMA_COMPAT_NEW, false ],
+ [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, true ],
+ [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, true ],
+ [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, true ],
+ ];
+ }
+
+ /**
+ * @covers \MediaWiki\Revision\RevisionStore::__construct
+ * @dataProvider provideMigrationConstruction
+ */
+ public function testMigrationConstruction( $migration, $expectException ) {
+ if ( $expectException ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+ }
+ $loadBalancer = $this->getMockLoadBalancer();
+ $blobStore = $this->getMockSqlBlobStore();
+ $cache = $this->getHashWANObjectCache();
+ $commentStore = $this->getMockCommentStore();
+ $services = MediaWikiServices::getInstance();
+ $nameTables = $services->getNameTableStoreFactory();
+ $contentModelStore = $nameTables->getContentModels();
+ $slotRoleStore = $nameTables->getSlotRoles();
+ $store = new RevisionStore(
+ $loadBalancer,
+ $blobStore,
+ $cache,
+ $commentStore,
+ $nameTables->getContentModels(),
+ $nameTables->getSlotRoles(),
+ $migration,
+ $services->getActorMigration()
+ );
+ if ( !$expectException ) {
+ $store = TestingAccessWrapper::newFromObject( $store );
+ $this->assertSame( $loadBalancer, $store->loadBalancer );
+ $this->assertSame( $blobStore, $store->blobStore );
+ $this->assertSame( $cache, $store->cache );
+ $this->assertSame( $commentStore, $store->commentStore );
+ $this->assertSame( $contentModelStore, $store->contentModelStore );
+ $this->assertSame( $slotRoleStore, $store->slotRoleStore );
+ $this->assertSame( $migration, $store->mcrMigrationStage );
+ }
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Revision;
+
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SuppressedDataException;
+use MediaWikiTestCase;
+use WikitextContent;
+
+/**
+ * @covers \MediaWiki\Revision\SlotRecord
+ */
+class SlotRecordTest extends MediaWikiTestCase {
+
+ private function makeRow( $data = [] ) {
+ $data = $data + [
+ 'slot_id' => 1234,
+ 'slot_content_id' => 33,
+ 'content_size' => '5',
+ 'content_sha1' => 'someHash',
+ 'content_address' => 'tt:456',
+ 'model_name' => CONTENT_MODEL_WIKITEXT,
+ 'format_name' => CONTENT_FORMAT_WIKITEXT,
+ 'slot_revision_id' => '2',
+ 'slot_origin' => '1',
+ 'role_name' => 'myRole',
+ ];
+ return (object)$data;
+ }
+
+ public function testCompleteConstruction() {
+ $row = $this->makeRow();
+ $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+ $this->assertTrue( $record->hasAddress() );
+ $this->assertTrue( $record->hasContentId() );
+ $this->assertTrue( $record->hasRevision() );
+ $this->assertTrue( $record->isInherited() );
+ $this->assertSame( 'A', $record->getContent()->getNativeData() );
+ $this->assertSame( 5, $record->getSize() );
+ $this->assertSame( 'someHash', $record->getSha1() );
+ $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+ $this->assertSame( 2, $record->getRevision() );
+ $this->assertSame( 1, $record->getOrigin() );
+ $this->assertSame( 'tt:456', $record->getAddress() );
+ $this->assertSame( 33, $record->getContentId() );
+ $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+ $this->assertSame( 'myRole', $record->getRole() );
+ }
+
+ public function testConstructionDeferred() {
+ $row = $this->makeRow( [
+ 'content_size' => null, // to be computed
+ 'content_sha1' => null, // to be computed
+ 'format_name' => function () {
+ return CONTENT_FORMAT_WIKITEXT;
+ },
+ 'slot_revision_id' => '2',
+ 'slot_origin' => '2',
+ 'slot_content_id' => function () {
+ return null;
+ },
+ ] );
+
+ $content = function () {
+ return new WikitextContent( 'A' );
+ };
+
+ $record = new SlotRecord( $row, $content );
+
+ $this->assertTrue( $record->hasAddress() );
+ $this->assertTrue( $record->hasRevision() );
+ $this->assertFalse( $record->hasContentId() );
+ $this->assertFalse( $record->isInherited() );
+ $this->assertSame( 'A', $record->getContent()->getNativeData() );
+ $this->assertSame( 1, $record->getSize() );
+ $this->assertNotNull( $record->getSha1() );
+ $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+ $this->assertSame( 2, $record->getRevision() );
+ $this->assertSame( 2, $record->getRevision() );
+ $this->assertSame( 'tt:456', $record->getAddress() );
+ $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
+ $this->assertSame( 'myRole', $record->getRole() );
+ }
+
+ public function testNewUnsaved() {
+ $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
+
+ $this->assertFalse( $record->hasAddress() );
+ $this->assertFalse( $record->hasContentId() );
+ $this->assertFalse( $record->hasRevision() );
+ $this->assertFalse( $record->isInherited() );
+ $this->assertFalse( $record->hasOrigin() );
+ $this->assertSame( 'A', $record->getContent()->getNativeData() );
+ $this->assertSame( 1, $record->getSize() );
+ $this->assertNotNull( $record->getSha1() );
+ $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
+ $this->assertSame( 'myRole', $record->getRole() );
+ }
+
+ public function provideInvalidConstruction() {
+ yield 'both null' => [ null, null ];
+ yield 'null row' => [ null, new WikitextContent( 'A' ) ];
+ yield 'array row' => [ [], new WikitextContent( 'A' ) ];
+ yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
+ yield 'null content' => [ (object)[], null ];
+ }
+
+ /**
+ * @dataProvider provideInvalidConstruction
+ */
+ public function testInvalidConstruction( $row, $content ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+ new SlotRecord( $row, $content );
+ }
+
+ public function testGetContentId_fails() {
+ $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $this->setExpectedException( IncompleteRevisionException::class );
+
+ $record->getContentId();
+ }
+
+ public function testGetAddress_fails() {
+ $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $this->setExpectedException( IncompleteRevisionException::class );
+
+ $record->getAddress();
+ }
+
+ public function provideIncomplete() {
+ $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ yield 'unsaved' => [ $unsaved ];
+
+ $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+ $inherited = SlotRecord::newInherited( $parent );
+ yield 'inherited' => [ $inherited ];
+ }
+
+ /**
+ * @dataProvider provideIncomplete
+ */
+ public function testGetRevision_fails( SlotRecord $record ) {
+ $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $this->setExpectedException( IncompleteRevisionException::class );
+
+ $record->getRevision();
+ }
+
+ /**
+ * @dataProvider provideIncomplete
+ */
+ public function testGetOrigin_fails( SlotRecord $record ) {
+ $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+ $this->setExpectedException( IncompleteRevisionException::class );
+
+ $record->getOrigin();
+ }
+
+ public function provideHashStability() {
+ yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
+ yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
+ }
+
+ /**
+ * @dataProvider provideHashStability
+ */
+ public function testHashStability( $text, $hash ) {
+ // Changing the output of the hash function will break things horribly!
+
+ $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
+
+ $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
+ $this->assertSame( $hash, $record->getSha1() );
+ }
+
+ public function testNewWithSuppressedContent() {
+ $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
+ $output = SlotRecord::newWithSuppressedContent( $input );
+
+ $this->setExpectedException( SuppressedDataException::class );
+ $output->getContent();
+ }
+
+ public function testNewInherited() {
+ $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
+ $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
+
+ // This would happen while doing an edit, before saving revision meta-data.
+ $inherited = SlotRecord::newInherited( $parent );
+
+ $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
+ $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
+ $this->assertSame( $parent->getContent(), $inherited->getContent() );
+ $this->assertTrue( $inherited->isInherited() );
+ $this->assertTrue( $inherited->hasOrigin() );
+ $this->assertFalse( $inherited->hasRevision() );
+
+ // make sure we didn't mess with the internal state of $parent
+ $this->assertFalse( $parent->isInherited() );
+ $this->assertSame( 7, $parent->getRevision() );
+
+ // This would happen while doing an edit, after saving the revision meta-data
+ // and content meta-data.
+ $saved = SlotRecord::newSaved(
+ 10,
+ $inherited->getContentId(),
+ $inherited->getAddress(),
+ $inherited
+ );
+ $this->assertSame( $parent->getContentId(), $saved->getContentId() );
+ $this->assertSame( $parent->getAddress(), $saved->getAddress() );
+ $this->assertSame( $parent->getContent(), $saved->getContent() );
+ $this->assertTrue( $saved->isInherited() );
+ $this->assertTrue( $saved->hasRevision() );
+ $this->assertSame( 10, $saved->getRevision() );
+
+ // make sure we didn't mess with the internal state of $parent or $inherited
+ $this->assertSame( 7, $parent->getRevision() );
+ $this->assertFalse( $inherited->hasRevision() );
+ }
+
+ public function testNewSaved() {
+ // This would happen while doing an edit, before saving revision meta-data.
+ $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+ // This would happen while doing an edit, after saving the revision meta-data
+ // and content meta-data.
+ $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
+ $this->assertFalse( $saved->isInherited() );
+ $this->assertTrue( $saved->hasOrigin() );
+ $this->assertTrue( $saved->hasRevision() );
+ $this->assertTrue( $saved->hasAddress() );
+ $this->assertTrue( $saved->hasContentId() );
+ $this->assertSame( 'theNewAddress', $saved->getAddress() );
+ $this->assertSame( 20, $saved->getContentId() );
+ $this->assertSame( 'A', $saved->getContent()->getNativeData() );
+ $this->assertSame( 10, $saved->getRevision() );
+ $this->assertSame( 10, $saved->getOrigin() );
+
+ // make sure we didn't mess with the internal state of $unsaved
+ $this->assertFalse( $unsaved->hasAddress() );
+ $this->assertFalse( $unsaved->hasContentId() );
+ $this->assertFalse( $unsaved->hasRevision() );
+ }
+
+ public function provideNewSaved_LogicException() {
+ $freshRow = $this->makeRow( [
+ 'content_id' => 10,
+ 'content_address' => 'address:1',
+ 'slot_origin' => 1,
+ 'slot_revision_id' => 1,
+ ] );
+
+ $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
+ yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
+ yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
+ yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
+
+ $inheritedRow = $this->makeRow( [
+ 'content_id' => null,
+ 'content_address' => null,
+ 'slot_origin' => 0,
+ 'slot_revision_id' => 1,
+ ] );
+
+ $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
+ yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
+ }
+
+ /**
+ * @dataProvider provideNewSaved_LogicException
+ */
+ public function testNewSaved_LogicException(
+ $revisionId,
+ $contentId,
+ $contentAddress,
+ SlotRecord $protoSlot
+ ) {
+ $this->setExpectedException( LogicException::class );
+ SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+ }
+
+ public function provideNewSaved_InvalidArgumentException() {
+ $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
+
+ yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
+ yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
+ yield 'bad content address' => [ 7, 5, 77, $unsaved ];
+ }
+
+ /**
+ * @dataProvider provideNewSaved_InvalidArgumentException
+ */
+ public function testNewSaved_InvalidArgumentException(
+ $revisionId,
+ $contentId,
+ $contentAddress,
+ SlotRecord $protoSlot
+ ) {
+ $this->setExpectedException( InvalidArgumentException::class );
+ SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
+ }
+
+ public function provideHasSameContent() {
+ $fail = function () {
+ self::fail( 'There should be no need to actually load the content.' );
+ };
+
+ $a100a1 = new SlotRecord(
+ $this->makeRow(
+ [
+ 'model_name' => 'A',
+ 'content_size' => 100,
+ 'content_sha1' => 'hash-a',
+ 'content_address' => 'xxx:a1',
+ ]
+ ),
+ $fail
+ );
+ $a100a1b = new SlotRecord(
+ $this->makeRow(
+ [
+ 'model_name' => 'A',
+ 'content_size' => 100,
+ 'content_sha1' => 'hash-a',
+ 'content_address' => 'xxx:a1',
+ ]
+ ),
+ $fail
+ );
+ $a100null = new SlotRecord(
+ $this->makeRow(
+ [
+ 'model_name' => 'A',
+ 'content_size' => 100,
+ 'content_sha1' => 'hash-a',
+ 'content_address' => null,
+ ]
+ ),
+ $fail
+ );
+ $a100a2 = new SlotRecord(
+ $this->makeRow(
+ [
+ 'model_name' => 'A',
+ 'content_size' => 100,
+ 'content_sha1' => 'hash-a',
+ 'content_address' => 'xxx:a2',
+ ]
+ ),
+ $fail
+ );
+ $b100a1 = new SlotRecord(
+ $this->makeRow(
+ [
+ 'model_name' => 'B',
+ 'content_size' => 100,
+ 'content_sha1' => 'hash-a',
+ 'content_address' => 'xxx:a1',
+ ]
+ ),
+ $fail
+ );
+ $a200a1 = new SlotRecord(
+ $this->makeRow(
+ [
+ 'model_name' => 'A',
+ 'content_size' => 200,
+ 'content_sha1' => 'hash-a',
+ 'content_address' => 'xxx:a2',
+ ]
+ ),
+ $fail
+ );
+ $a100x1 = new SlotRecord(
+ $this->makeRow(
+ [
+ 'model_name' => 'A',
+ 'content_size' => 100,
+ 'content_sha1' => 'hash-x',
+ 'content_address' => 'xxx:x1',
+ ]
+ ),
+ $fail
+ );
+
+ yield 'same instance' => [ $a100a1, $a100a1, true ];
+ yield 'no address' => [ $a100a1, $a100null, true ];
+ yield 'same address' => [ $a100a1, $a100a1b, true ];
+ yield 'different address' => [ $a100a1, $a100a2, true ];
+ yield 'different model' => [ $a100a1, $b100a1, false ];
+ yield 'different size' => [ $a100a1, $a200a1, false ];
+ yield 'different hash' => [ $a100a1, $a100x1, false ];
+ }
+
+ /**
+ * @dataProvider provideHasSameContent
+ */
+ public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
+ $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
+ $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
+ }
+
+}
--- /dev/null
+ALTER TABLE /*_*/revision ADD rev_text_id INTEGER DEFAULT 0;
+ALTER TABLE /*_*/revision ADD rev_content_model VARBINARY(32) DEFAULT NULL;
+ALTER TABLE /*_*/revision ADD rev_content_format VARBINARY(64) DEFAULT NULL;
--- /dev/null
+DROP TABLE /*_*/slots;
+DROP TABLE /*_*/content;
+DROP TABLE /*_*/content_models;
+DROP TABLE /*_*/slot_roles;
--- /dev/null
+ALTER TABLE /*_*/revision DROP COLUMN rev_text_id;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_model;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_format;
--- /dev/null
+DROP TABLE /*_*/revision;
+
+CREATE TABLE /*_*/revision (
+ rev_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ rev_page INTEGER NOT NULL,
+ rev_comment BLOB NOT NULL,
+ rev_user INTEGER NOT NULL default 0,
+ rev_user_text varchar(255) NOT NULL default '',
+ rev_timestamp blob(14) NOT NULL default '',
+ rev_minor_edit INTEGER NOT NULL default 0,
+ rev_deleted INTEGER NOT NULL default 0,
+ rev_len INTEGER unsigned,
+ rev_parent_id INTEGER default NULL,
+ rev_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
<?php
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\IncompleteRevisionException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
/**
* RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
<?php
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Tests\Storage\McrSchemaOverride;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Tests\Revision\McrSchemaOverride;
/**
* Tests Revision against the MCR DB schema after schema migration.
<?php
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Tests\Storage\McrReadNewSchemaOverride;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Tests\Revision\McrReadNewSchemaOverride;
/**
* Tests Revision against the intermediate MCR DB schema for use during schema migration.
<?php
-use MediaWiki\Tests\Storage\McrWriteBothSchemaOverride;
+use MediaWiki\Tests\Revision\McrWriteBothSchemaOverride;
/**
* Tests Revision against the intermediate MCR DB schema for use during schema migration.
<?php
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
/**
* Tests Revision against the pre-MCR, pre ContentHandler DB schema.
<?php
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
/**
* Tests Revision against the pre-MCR DB schema.
<?php
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
use MediaWiki\Storage\SqlBlobStore;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LoadBalancer;
/**
* @dataProvider provideConstructFromArray
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromArray( $rowArray ) {
$rev = new Revision( $rowArray, 0, $this->getMockTitle() );
/**
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromEmptyArray() {
$rev = new Revision( [], 0, $this->getMockTitle() );
/**
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromArrayWithBadPageId() {
Wikimedia\suppressWarnings();
/**
* @dataProvider provideConstructFromArray_userSetAsExpected
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*
* @param array $rowArray
* @param mixed $expectedUserId null to expect the current wgUser ID
/**
* @dataProvider provideConstructFromArrayThrowsExceptions
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
$this->setExpectedException(
/**
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromNothing() {
$this->setExpectedException(
/**
* @dataProvider provideConstructFromRow
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromRow( array $arrayData, callable $assertions ) {
$row = (object)$arrayData;
/**
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromRowWithBadPageId() {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
use ContentHandler;
use LinksUpdate;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\DerivedPageDataUpdater;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionRecord;
use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\SlotRecord;
use MediaWikiTestCase;
use MWCallableUpdate;
use MWTimestamp;
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use TextContent;
-use Title;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class McrReadNewRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
- use McrReadNewSchemaOverride;
-
- protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
- $numberOfSlots = count( $rev->getSlotRoles() );
-
- // new schema is written
- $this->assertSelect(
- 'slots',
- [ 'count(*)' ],
- [ 'slot_revision_id' => $rev->getId() ],
- [ [ (string)$numberOfSlots ] ]
- );
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
-
- $this->assertSelect(
- $revQuery['tables'],
- [ 'count(*)' ],
- [
- 'slot_revision_id' => $rev->getId(),
- ],
- [ [ (string)$numberOfSlots ] ],
- [],
- $revQuery['joins']
- );
-
- // Legacy schema is still being written
- $this->assertSelect(
- [ 'revision', 'text' ],
- [ 'count(*)' ],
- [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
- [ [ 1 ] ],
- [],
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
- );
-
- parent::assertRevisionExistsInDatabase( $rev );
- }
-
- /**
- * @param SlotRecord $a
- * @param SlotRecord $b
- */
- protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
- parent::assertSameSlotContent( $a, $b );
-
- // Assert that the same content ID has been used
- $this->assertSame( $a->getContentId(), $b->getContentId() );
- }
-
- public function provideInsertRevisionOn_successes() {
- foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
- yield $case;
- }
-
- yield 'Multi-slot revision insertion' => [
- [
- 'content' => [
- 'main' => new WikitextContent( 'Chicken' ),
- 'aux' => new TextContent( 'Egg' ),
- ],
- 'page' => true,
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- ];
- }
-
- public function provideNewNullRevision() {
- foreach ( parent::provideNewNullRevision() as $case ) {
- yield $case;
- }
-
- yield [
- Title::newFromText( 'UTPage_notAutoCreated' ),
- [
- 'content' => [
- 'main' => new WikitextContent( 'Chicken' ),
- 'aux' => new WikitextContent( 'Omelet' ),
- ],
- ],
- CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
- ];
- }
-
- public function testGetQueryInfo_NoSlotDataJoin() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $queryInfo = $store->getQueryInfo();
-
- // with the new schema enabled, query info should not join the main slot info
- $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
- $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
- }
-
- public function provideNewMutableRevisionFromArray() {
- foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
- yield $case;
- }
-
- yield 'Basic array, multiple roles' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 29,
- 'parent_id' => 1,
- 'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
- 'comment' => 'Goat Comment!',
- 'content' => [
- 'main' => new WikitextContent( 'Söme Cöntent' ),
- 'aux' => new TextContent( 'Öther Cöntent' ),
- ]
- ]
- ];
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the intermediate MCR database
- * schema for use during schema migration.
- */
-trait McrReadNewSchemaOverride {
-
- use PatchFileLocation;
- use McrSchemaDetection;
-
- /**
- * @return int
- */
- protected function getMcrMigrationStage() {
- return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
- }
-
- /**
- * @return string[]
- */
- protected function getMcrTablesToReset() {
- return [ 'content', 'content_models', 'slots', 'slot_roles' ];
- }
-
- /**
- * @return array[]
- */
- protected function getSchemaOverrides( IMaintainableDatabase $db ) {
- $overrides = [
- 'scripts' => [],
- 'drop' => [],
- 'create' => [],
- 'alter' => [],
- ];
-
- if ( !$this->hasMcrTables( $db ) ) {
- $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
- }
-
- if ( !$this->hasPreMcrFields( $db ) ) {
- $overrides['alter'][] = 'revision';
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
- }
-
- return $overrides;
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use TextContent;
-use Title;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the post-migration MCR DB schema.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
- use McrSchemaOverride;
-
- protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
- $numberOfSlots = count( $rev->getSlotRoles() );
-
- // new schema is written
- $this->assertSelect(
- 'slots',
- [ 'count(*)' ],
- [ 'slot_revision_id' => $rev->getId() ],
- [ [ (string)$numberOfSlots ] ]
- );
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
-
- $this->assertSelect(
- $revQuery['tables'],
- [ 'count(*)' ],
- [
- 'slot_revision_id' => $rev->getId(),
- ],
- [ [ (string)$numberOfSlots ] ],
- [],
- $revQuery['joins']
- );
-
- parent::assertRevisionExistsInDatabase( $rev );
- }
-
- /**
- * @param SlotRecord $a
- * @param SlotRecord $b
- */
- protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
- parent::assertSameSlotContent( $a, $b );
-
- // Assert that the same content ID has been used
- $this->assertSame( $a->getContentId(), $b->getContentId() );
- }
-
- public function provideInsertRevisionOn_successes() {
- foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
- yield $case;
- }
-
- yield 'Multi-slot revision insertion' => [
- [
- 'content' => [
- 'main' => new WikitextContent( 'Chicken' ),
- 'aux' => new TextContent( 'Egg' ),
- ],
- 'page' => true,
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- ];
- }
-
- public function provideNewNullRevision() {
- foreach ( parent::provideNewNullRevision() as $case ) {
- yield $case;
- }
-
- yield [
- Title::newFromText( 'UTPage_notAutoCreated' ),
- [
- 'content' => [
- 'main' => new WikitextContent( 'Chicken' ),
- 'aux' => new WikitextContent( 'Omelet' ),
- ],
- ],
- CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
- ];
- }
-
- public function provideNewMutableRevisionFromArray() {
- foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
- yield $case;
- }
-
- yield 'Basic array, multiple roles' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 29,
- 'parent_id' => 1,
- 'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
- 'comment' => 'Goat Comment!',
- 'content' => [
- 'main' => new WikitextContent( 'Söme Cöntent' ),
- 'aux' => new TextContent( 'Öther Cöntent' ),
- ]
- ]
- ];
- }
-
- public function testGetQueryInfo_NoSlotDataJoin() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $queryInfo = $store->getQueryInfo();
-
- // with the new schema enabled, query info should not join the main slot info
- $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
- $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
- * @covers \MediaWiki\Storage\RevisionStore::insertSlotRowOn
- * @covers \MediaWiki\Storage\RevisionStore::insertContentRowOn
- */
- public function testInsertRevisionOn_T202032() {
- // This test only makes sense for MySQL
- if ( $this->db->getType() !== 'mysql' ) {
- $this->assertTrue( true );
- return;
- }
-
- // NOTE: must be done before checking MAX(rev_id)
- $page = $this->getTestPage();
-
- $maxRevId = $this->db->selectField( 'revision', 'MAX(rev_id)' );
-
- // Construct a slot row that will conflict with the insertion of the next revision ID,
- // to emulate the failure mode described in T202032. Nothing will ever read this row,
- // we just need it to trigger a primary key conflict.
- $this->db->insert( 'slots', [
- 'slot_revision_id' => $maxRevId + 1,
- 'slot_role_id' => 1,
- 'slot_content_id' => 0,
- 'slot_origin' => 0
- ], __METHOD__ );
-
- $rev = new MutableRevisionRecord( $page->getTitle() );
- $rev->setTimestamp( '20180101000000' );
- $rev->setComment( CommentStoreComment::newUnsavedComment( 'test' ) );
- $rev->setUser( $this->getTestUser()->getUser() );
- $rev->setContent( 'main', new WikitextContent( 'Text' ) );
- $rev->setPageId( $page->getId() );
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $return = $store->insertRevisionOn( $rev, $this->db );
-
- $this->assertSame( $maxRevId + 2, $return->getId() );
-
- // is the new revision correct?
- $this->assertRevisionCompleteness( $return );
- $this->assertRevisionRecordsEqual( $rev, $return );
-
- // can we find it directly in the database?
- $this->assertRevisionExistsInDatabase( $return );
-
- // can we load it from the store?
- $loaded = $store->getRevisionById( $return->getId() );
- $this->assertRevisionCompleteness( $loaded );
- $this->assertRevisionRecordsEqual( $return, $loaded );
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IDatabase;
-
-/**
- * Trait providing methods for detecting which MCR schema migration phase the current schema
- * is compatible with.
- */
-trait McrSchemaDetection {
-
- /**
- * Returns true if MCR-related tables exist in the database.
- * If yes, the database is compatible with with MIGRATION_NEW.
- * If hasPreMcrFields() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
- *
- * @param IDatabase $db
- * @return bool
- */
- protected function hasMcrTables( IDatabase $db ) {
- return $db->tableExists( 'slots', __METHOD__ );
- }
-
- /**
- * Returns true if pre-MCR fields still exist in the database.
- * If yes, the database is compatible with with MIGRATION_OLD mode.
- * If hasMcrTables() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
- *
- * Note that if the database has been updated in MIGRATION_NEW mode,
- * the rev_text_id field will be 0 for new revisions. This means that
- * in MIGRATION_OLD mode, reading such revisions will fail, even though
- * all the necessary fields exist.
- * This is not relevant for unit tests, since unit tests reset the database content anyway.
- *
- * @param IDatabase $db
- * @return bool
- */
- protected function hasPreMcrFields( IDatabase $db ) {
- return $db->fieldExists( 'revision', 'rev_content_model', __METHOD__ );
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the post-migration
- * MCR database schema.
- */
-trait McrSchemaOverride {
-
- use PatchFileLocation;
- use McrSchemaDetection;
-
- /**
- * @return int
- */
- protected function getMcrMigrationStage() {
- return MIGRATION_NEW;
- }
-
- /**
- * @return string[]
- */
- protected function getMcrTablesToReset() {
- return [
- 'content',
- 'content_models',
- 'slots',
- 'slot_roles',
- ];
- }
-
- protected function getSchemaOverrides( IMaintainableDatabase $db ) {
- $overrides = [
- 'scripts' => [],
- 'drop' => [],
- 'create' => [],
- 'alter' => [],
- ];
-
- if ( !$this->hasMcrTables( $db ) ) {
- $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots.sql' );
- }
-
- if ( !$this->hasPreMcrFields( $db ) ) {
- $overrides['alter'][] = 'revision';
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'drop-pre-mcr-fields', __DIR__ );
- }
-
- return $overrides;
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
-use Revision;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
- use McrWriteBothSchemaOverride;
-
- protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
- $row = parent::revisionToRow( $rev, $options );
-
- $row->rev_text_id = (string)$rev->getTextId();
- $row->rev_content_format = (string)$rev->getContentFormat();
- $row->rev_content_model = (string)$rev->getContentModel();
-
- return $row;
- }
-
- protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
- // New schema is being written
- $this->assertSelect(
- 'slots',
- [ 'count(*)' ],
- [ 'slot_revision_id' => $rev->getId() ],
- [ [ '1' ] ]
- );
-
- $this->assertSelect(
- 'content',
- [ 'count(*)' ],
- [ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
- [ [ '1' ] ]
- );
-
- // Legacy schema is still being written
- $this->assertSelect(
- [ 'revision', 'text' ],
- [ 'count(*)' ],
- [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
- [ [ 1 ] ],
- [],
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
- );
-
- parent::assertRevisionExistsInDatabase( $rev );
- }
-
- /**
- * @param SlotRecord $a
- * @param SlotRecord $b
- */
- protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
- parent::assertSameSlotContent( $a, $b );
-
- // Assert that the same content ID has been used
- if ( $a->hasContentId() && $b->hasContentId() ) {
- $this->assertSame( $a->getContentId(), $b->getContentId() );
- }
- }
-
- public function provideInsertRevisionOn_failures() {
- foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
- yield $case;
- }
-
- yield 'slot that is not main slot' => [
- [
- 'content' => [
- 'main' => new WikitextContent( 'Chicken' ),
- 'lalala' => new WikitextContent( 'Duck' ),
- ],
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- new InvalidArgumentException( 'Only the main slot is supported' )
- ];
- }
-
- public function provideNewMutableRevisionFromArray() {
- foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
- yield $case;
- }
-
- yield 'Basic array, with page & id' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'text_id' => 2,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'content_format' => 'text/x-wiki',
- 'content_model' => 'wikitext',
- ]
- ];
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
- * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
- */
- public function testInsertRevisionFromArchiveRow_unmigratedArchiveRow() {
- // The main purpose of this test is to assert that after reading an archive
- // row using the old schema it can be inserted into the revision table,
- // and a slot row is created based on slot emulated from the old-style archive row,
- // when none such slot row exists yet.
-
- $title = $this->getTestPage()->getTitle();
-
- $this->db->insert(
- 'text',
- [ 'old_text' => 'Just a test', 'old_flags' => 'utf-8' ],
- __METHOD__
- );
-
- $textId = $this->db->insertId();
-
- $row = (object)[
- 'ar_minor_edit' => '0',
- 'ar_user' => '0',
- 'ar_user_text' => '127.0.0.1',
- 'ar_actor' => null,
- 'ar_len' => '11',
- 'ar_deleted' => '0',
- 'ar_rev_id' => 112277,
- 'ar_timestamp' => $this->db->timestamp( '20180101000000' ),
- 'ar_sha1' => 'deadbeef',
- 'ar_page_id' => $title->getArticleID(),
- 'ar_comment_text' => 'just a test',
- 'ar_comment_data' => null,
- 'ar_comment_cid' => null,
- 'ar_content_format' => null,
- 'ar_content_model' => null,
- 'ts_tags' => null,
- 'ar_id' => 17,
- 'ar_namespace' => $title->getNamespace(),
- 'ar_title' => $title->getDBkey(),
- 'ar_text_id' => $textId,
- 'ar_parent_id' => 112211,
- ];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $rev = $store->newRevisionFromArchiveRow( $row );
-
- // re-insert archived revision
- $return = $store->insertRevisionOn( $rev, $this->db );
-
- // is the new revision correct?
- $this->assertRevisionCompleteness( $return );
- $this->assertRevisionRecordsEqual( $rev, $return );
-
- // can we load it from the store?
- $loaded = $store->getRevisionById( $return->getId() );
- $this->assertNotNull( $loaded );
- $this->assertRevisionCompleteness( $loaded );
- $this->assertRevisionRecordsEqual( $return, $loaded );
-
- // can we find it directly in the database?
- $this->assertRevisionExistsInDatabase( $return );
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the intermediate MCR database
- * schema for use during schema migration.
- */
-trait McrWriteBothSchemaOverride {
-
- use PatchFileLocation;
- use McrSchemaDetection;
-
- /**
- * @return int
- */
- protected function getMcrMigrationStage() {
- return SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD;
- }
-
- /**
- * @return string[]
- */
- protected function getMcrTablesToReset() {
- return [ 'content', 'content_models', 'slots', 'slot_roles' ];
- }
-
- /**
- * @return array[]
- */
- protected function getSchemaOverrides( IMaintainableDatabase $db ) {
- $overrides = [
- 'scripts' => [],
- 'drop' => [],
- 'create' => [],
- 'alter' => [],
- ];
-
- if ( !$this->hasMcrTables( $db ) ) {
- $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
- }
-
- if ( !$this->hasPreMcrFields( $db ) ) {
- $overrides['alter'][] = 'revision';
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
- }
-
- return $overrides;
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use TextContent;
-use Title;
-use User;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Storage\MutableRevisionRecord
- * @covers \MediaWiki\Storage\RevisionRecord
- */
-class MutableRevisionRecordTest extends MediaWikiTestCase {
-
- use RevisionRecordTests;
-
- /**
- * @param array $rowOverrides
- *
- * @return MutableRevisionRecord
- */
- protected function newRevision( array $rowOverrides = [] ) {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- $user = new UserIdentityValue( 11, 'Tester', 0 );
- $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
- $record = new MutableRevisionRecord( $title );
-
- if ( isset( $rowOverrides['rev_deleted'] ) ) {
- $record->setVisibility( $rowOverrides['rev_deleted'] );
- }
-
- if ( isset( $rowOverrides['rev_id'] ) ) {
- $record->setId( $rowOverrides['rev_id'] );
- }
-
- if ( isset( $rowOverrides['rev_page'] ) ) {
- $record->setPageId( $rowOverrides['rev_page'] );
- }
-
- $record->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
- $record->setComment( $comment );
- $record->setUser( $user );
- $record->setTimestamp( '20101010000000' );
-
- return $record;
- }
-
- public function provideConstructor() {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- yield [
- $title,
- 'acmewiki'
- ];
- }
-
- /**
- * @dataProvider provideConstructor
- *
- * @param Title $title
- * @param bool $wikiId
- */
- public function testConstructorAndGetters(
- Title $title,
- $wikiId = false
- ) {
- $rec = new MutableRevisionRecord( $title, $wikiId );
-
- $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
- $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
- }
-
- public function provideConstructorFailure() {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- yield 'not a wiki id' => [
- $title,
- null
- ];
- }
-
- /**
- * @dataProvider provideConstructorFailure
- *
- * @param Title $title
- * @param bool $wikiId
- */
- public function testConstructorFailure(
- Title $title,
- $wikiId = false
- ) {
- $this->setExpectedException( InvalidArgumentException::class );
- new MutableRevisionRecord( $title, $wikiId );
- }
-
- public function testSetGetId() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertNull( $record->getId() );
- $record->setId( 888 );
- $this->assertSame( 888, $record->getId() );
- }
-
- public function testSetGetUser() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $user = $this->getTestSysop()->getUser();
- $this->assertNull( $record->getUser() );
- $record->setUser( $user );
- $this->assertSame( $user, $record->getUser() );
- }
-
- public function testSetGetPageId() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertSame( 0, $record->getPageId() );
- $record->setPageId( 999 );
- $this->assertSame( 999, $record->getPageId() );
- }
-
- public function testSetGetParentId() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertNull( $record->getParentId() );
- $record->setParentId( 100 );
- $this->assertSame( 100, $record->getParentId() );
- }
-
- public function testGetMainContentWhenEmpty() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->setExpectedException( RevisionAccessException::class );
- $this->assertNull( $record->getContent( SlotRecord::MAIN ) );
- }
-
- public function testSetGetMainContent() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $content = new WikitextContent( 'Badger' );
- $record->setContent( SlotRecord::MAIN, $content );
- $this->assertSame( $content, $record->getContent( SlotRecord::MAIN ) );
- }
-
- public function testGetSlotWhenEmpty() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertFalse( $record->hasSlot( SlotRecord::MAIN ) );
-
- $this->setExpectedException( RevisionAccessException::class );
- $record->getSlot( SlotRecord::MAIN );
- }
-
- public function testSetGetSlot() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $slot = SlotRecord::newUnsaved(
- SlotRecord::MAIN,
- new WikitextContent( 'x' )
- );
- $record->setSlot( $slot );
- $this->assertTrue( $record->hasSlot( SlotRecord::MAIN ) );
- $this->assertSame( $slot, $record->getSlot( SlotRecord::MAIN ) );
- }
-
- public function testSetGetMinor() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertFalse( $record->isMinor() );
- $record->setMinorEdit( true );
- $this->assertSame( true, $record->isMinor() );
- }
-
- public function testSetGetTimestamp() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertNull( $record->getTimestamp() );
- $record->setTimestamp( '20180101010101' );
- $this->assertSame( '20180101010101', $record->getTimestamp() );
- }
-
- public function testSetGetVisibility() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertSame( 0, $record->getVisibility() );
- $record->setVisibility( RevisionRecord::DELETED_USER );
- $this->assertSame( RevisionRecord::DELETED_USER, $record->getVisibility() );
- }
-
- public function testSetGetSha1() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertSame( 'phoiac9h4m842xq45sp7s6u21eteeq1', $record->getSha1() );
- $record->setSha1( 'someHash' );
- $this->assertSame( 'someHash', $record->getSha1() );
- }
-
- public function testGetSlots() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertInstanceOf( MutableRevisionSlots::class, $record->getSlots() );
- }
-
- public function testSetGetSize() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $this->assertSame( 0, $record->getSize() );
- $record->setSize( 775 );
- $this->assertSame( 775, $record->getSize() );
- }
-
- public function testSetGetComment() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $comment = new CommentStoreComment( 1, 'foo' );
- $this->assertNull( $record->getComment() );
- $record->setComment( $comment );
- $this->assertSame( $comment, $record->getComment() );
- }
-
- public function testSimpleGetOriginalAndInheritedSlots() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $mainSlot = new SlotRecord(
- (object)[
- 'slot_id' => 1,
- 'slot_revision_id' => null, // unsaved
- 'slot_content_id' => 1,
- 'content_address' => null, // touched
- 'model_name' => 'x',
- 'role_name' => 'main',
- 'slot_origin' => null // touched
- ],
- new WikitextContent( 'main' )
- );
- $auxSlot = new SlotRecord(
- (object)[
- 'slot_id' => 2,
- 'slot_revision_id' => null, // unsaved
- 'slot_content_id' => 1,
- 'content_address' => 'foo', // inherited
- 'model_name' => 'x',
- 'role_name' => 'aux',
- 'slot_origin' => 1 // inherited
- ],
- new WikitextContent( 'aux' )
- );
-
- $record->setSlot( $mainSlot );
- $record->setSlot( $auxSlot );
-
- $this->assertSame( [ 'main' ], $record->getOriginalSlots()->getSlotRoles() );
- $this->assertSame( $mainSlot, $record->getOriginalSlots()->getSlot( SlotRecord::MAIN ) );
-
- $this->assertSame( [ 'aux' ], $record->getInheritedSlots()->getSlotRoles() );
- $this->assertSame( $auxSlot, $record->getInheritedSlots()->getSlot( 'aux' ) );
- }
-
- public function testSimpleremoveSlot() {
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
-
- $a = new WikitextContent( 'a' );
- $b = new WikitextContent( 'b' );
-
- $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
- $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
-
- $record->removeSlot( 'b' );
-
- $this->assertTrue( $record->hasSlot( 'a' ) );
- $this->assertFalse( $record->hasSlot( 'b' ) );
- }
-
- public function testApplyUpdate() {
- $update = new RevisionSlotsUpdate();
-
- $a = new WikitextContent( 'a' );
- $b = new WikitextContent( 'b' );
- $c = new WikitextContent( 'c' );
- $x = new WikitextContent( 'x' );
-
- $update->modifyContent( 'b', $x );
- $update->modifyContent( 'c', $x );
- $update->removeSlot( 'c' );
- $update->removeSlot( 'd' );
-
- $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
- $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
- $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
- $record->inheritSlot( SlotRecord::newSaved( 7, 5, 'c', SlotRecord::newUnsaved( 'c', $c ) ) );
-
- $record->applyUpdate( $update );
-
- $this->assertEquals( [ 'b' ], array_keys( $record->getOriginalSlots()->getSlots() ) );
- $this->assertEquals( $a, $record->getSlot( 'a' )->getContent() );
- $this->assertEquals( $x, $record->getSlot( 'b' )->getContent() );
- $this->assertFalse( $record->hasSlot( 'c' ) );
- }
-
- public function provideNotReadyForInsertion() {
- /** @var Title $title */
- $title = $this->getMock( Title::class );
-
- /** @var User $user */
- $user = $this->getMock( User::class );
-
- /** @var CommentStoreComment $comment */
- $comment = $this->getMockBuilder( CommentStoreComment::class )
- ->disableOriginalConstructor()
- ->getMock();
-
- $content = new TextContent( 'Test' );
-
- $rev = new MutableRevisionRecord( $title );
- yield 'empty' => [ $rev ];
-
- $rev = new MutableRevisionRecord( $title );
- $rev->setContent( SlotRecord::MAIN, $content );
- $rev->setUser( $user );
- $rev->setComment( $comment );
- yield 'no timestamp' => [ $rev ];
-
- $rev = new MutableRevisionRecord( $title );
- $rev->setUser( $user );
- $rev->setComment( $comment );
- $rev->setTimestamp( '20101010000000' );
- yield 'no content' => [ $rev ];
-
- $rev = new MutableRevisionRecord( $title );
- $rev->setContent( SlotRecord::MAIN, $content );
- $rev->setComment( $comment );
- $rev->setTimestamp( '20101010000000' );
- yield 'no user' => [ $rev ];
-
- $rev = new MutableRevisionRecord( $title );
- $rev->setUser( $user );
- $rev->setContent( SlotRecord::MAIN, $content );
- $rev->setTimestamp( '20101010000000' );
- yield 'no comment' => [ $rev ];
- }
-
- /**
- * @dataProvider provideNotReadyForInsertion
- */
- public function testNotReadyForInsertion( $rev ) {
- $this->assertFalse( $rev->isReadyForInsertion() );
- }
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use Content;
-use InvalidArgumentException;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\SlotRecord;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Storage\MutableRevisionSlots
- */
-class MutableRevisionSlotsTest extends RevisionSlotsTest {
-
- /**
- * @param SlotRecord[] $slots
- * @return RevisionSlots
- */
- protected function newRevisionSlots( $slots = [] ) {
- return new MutableRevisionSlots( $slots );
- }
-
- public function provideConstructorFailue() {
- yield 'array or the wrong thing' => [
- [ 1, 2, 3 ]
- ];
- }
-
- /**
- * @dataProvider provideConstructorFailue
- * @param $slots
- *
- * @covers \MediaWiki\Storage\RevisionSlots::__construct
- * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal
- */
- public function testConstructorFailue( $slots ) {
- $this->setExpectedException( InvalidArgumentException::class );
-
- new MutableRevisionSlots( $slots );
- }
-
- public function testSetMultipleSlots() {
- $slots = new MutableRevisionSlots();
-
- $this->assertSame( [], $slots->getSlots() );
-
- $slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) );
- $slots->setSlot( $slotA );
- $this->assertTrue( $slots->hasSlot( 'some' ) );
- $this->assertSame( $slotA, $slots->getSlot( 'some' ) );
- $this->assertSame( [ 'some' => $slotA ], $slots->getSlots() );
-
- $slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) );
- $slots->setSlot( $slotB );
- $this->assertTrue( $slots->hasSlot( 'other' ) );
- $this->assertSame( $slotB, $slots->getSlot( 'other' ) );
- $this->assertSame( [ 'some' => $slotA, 'other' => $slotB ], $slots->getSlots() );
- }
-
- public function testSetExistingSlotOverwritesSlot() {
- $slots = new MutableRevisionSlots();
-
- $this->assertSame( [], $slots->getSlots() );
-
- $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $slots->setSlot( $slotA );
- $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
- $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
-
- $slotB = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'B' ) );
- $slots->setSlot( $slotB );
- $this->assertSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
- $this->assertSame( [ 'main' => $slotB ], $slots->getSlots() );
- }
-
- /**
- * @param string $role
- * @param Content $content
- * @return SlotRecord
- */
- private function newSavedSlot( $role, Content $content ) {
- return SlotRecord::newSaved( 7, 7, 'xyz', SlotRecord::newUnsaved( $role, $content ) );
- }
-
- public function testInheritSlotOverwritesSlot() {
- $slots = new MutableRevisionSlots();
- $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $slots->setSlot( $slotA );
- $slotB = $this->newSavedSlot( SlotRecord::MAIN, new WikitextContent( 'B' ) );
- $slotC = $this->newSavedSlot( 'foo', new WikitextContent( 'C' ) );
- $slots->inheritSlot( $slotB );
- $slots->inheritSlot( $slotC );
- $this->assertSame( [ 'main', 'foo' ], $slots->getSlotRoles() );
- $this->assertNotSame( $slotB, $slots->getSlot( SlotRecord::MAIN ) );
- $this->assertNotSame( $slotC, $slots->getSlot( 'foo' ) );
- $this->assertTrue( $slots->getSlot( SlotRecord::MAIN )->isInherited() );
- $this->assertTrue( $slots->getSlot( 'foo' )->isInherited() );
- $this->assertSame( $slotB->getContent(), $slots->getSlot( SlotRecord::MAIN )->getContent() );
- $this->assertSame( $slotC->getContent(), $slots->getSlot( 'foo' )->getContent() );
- }
-
- public function testSetContentOfExistingSlotOverwritesContent() {
- $slots = new MutableRevisionSlots();
-
- $this->assertSame( [], $slots->getSlots() );
-
- $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $slots->setSlot( $slotA );
- $this->assertSame( $slotA, $slots->getSlot( SlotRecord::MAIN ) );
- $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
-
- $newContent = new WikitextContent( 'B' );
- $slots->setContent( SlotRecord::MAIN, $newContent );
- $this->assertSame( $newContent, $slots->getContent( SlotRecord::MAIN ) );
- }
-
- public function testRemoveExistingSlot() {
- $slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $slots = new MutableRevisionSlots( [ $slotA ] );
-
- $this->assertSame( [ 'main' => $slotA ], $slots->getSlots() );
-
- $slots->removeSlot( SlotRecord::MAIN );
- $this->assertSame( [], $slots->getSlots() );
- $this->setExpectedException( RevisionAccessException::class );
- $slots->getSlot( SlotRecord::MAIN );
- }
-
- public function testNewFromParentRevisionSlots() {
- /** @var SlotRecord[] $parentSlots */
- $parentSlots = [
- 'some' => $this->newSavedSlot( 'some', new WikitextContent( 'X' ) ),
- 'other' => $this->newSavedSlot( 'other', new WikitextContent( 'Y' ) ),
- ];
- $slots = MutableRevisionSlots::newFromParentRevisionSlots( $parentSlots );
- $this->assertSame( [ 'some', 'other' ], $slots->getSlotRoles() );
- $this->assertNotSame( $parentSlots['some'], $slots->getSlot( 'some' ) );
- $this->assertNotSame( $parentSlots['other'], $slots->getSlot( 'other' ) );
- $this->assertTrue( $slots->getSlot( 'some' )->isInherited() );
- $this->assertTrue( $slots->getSlot( 'other' )->isInherited() );
- $this->assertSame( $parentSlots['some']->getContent(), $slots->getContent( 'some' ) );
- $this->assertSame( $parentSlots['other']->getContent(), $slots->getContent( 'other' ) );
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Revision;
-
-/**
- * Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
- use PreMcrSchemaOverride;
-
- protected function getContentHandlerUseDB() {
- return false;
- }
-
- protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
- $row = parent::revisionToRow( $rev, $options );
-
- $row->rev_text_id = (string)$rev->getTextId();
-
- return $row;
- }
-
- public function provideGetArchiveQueryInfo() {
- yield [
- [
- 'tables' => [ 'archive' ],
- 'fields' => array_merge(
- $this->getDefaultArchiveFields(),
- [
- 'ar_comment_text' => 'ar_comment',
- 'ar_comment_data' => 'NULL',
- 'ar_comment_cid' => 'NULL',
- 'ar_user_text' => 'ar_user_text',
- 'ar_user' => 'ar_user',
- 'ar_actor' => 'NULL',
- ]
- ),
- 'joins' => [],
- ]
- ];
- }
-
- public function provideGetQueryInfo() {
- yield [
- [],
- [
- 'tables' => [ 'revision' ],
- 'fields' => array_merge(
- $this->getDefaultQueryFields(),
- $this->getCommentQueryFields(),
- $this->getActorQueryFields()
- ),
- 'joins' => [],
- ]
- ];
- yield [
- [ 'page' ],
- [
- 'tables' => [ 'revision', 'page' ],
- 'fields' => array_merge(
- $this->getDefaultQueryFields(),
- $this->getCommentQueryFields(),
- $this->getActorQueryFields(),
- [
- 'page_namespace',
- 'page_title',
- 'page_id',
- 'page_latest',
- 'page_is_redirect',
- 'page_len',
- ]
- ),
- 'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
- ],
- ]
- ];
- yield [
- [ 'user' ],
- [
- 'tables' => [ 'revision', 'user' ],
- 'fields' => array_merge(
- $this->getDefaultQueryFields(),
- $this->getCommentQueryFields(),
- $this->getActorQueryFields(),
- [
- 'user_name',
- ]
- ),
- 'joins' => [
- 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
- ],
- ]
- ];
- yield [
- [ 'text' ],
- [
- 'tables' => [ 'revision', 'text' ],
- 'fields' => array_merge(
- $this->getDefaultQueryFields(),
- $this->getCommentQueryFields(),
- $this->getActorQueryFields(),
- [
- 'old_text',
- 'old_flags',
- ]
- ),
- 'joins' => [
- 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
- ],
- ]
- ];
- }
-
- public function provideGetSlotsQueryInfo() {
- $db = wfGetDB( DB_REPLICA );
-
- yield [
- [],
- [
- 'tables' => [
- 'slots' => 'revision',
- ],
- 'fields' => array_merge(
- [
- 'slot_revision_id' => 'slots.rev_id',
- 'slot_content_id' => 'NULL',
- 'slot_origin' => 'slots.rev_id',
- 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
- ]
- ),
- 'joins' => [],
- ]
- ];
- yield [
- [ 'content' ],
- [
- 'tables' => [
- 'slots' => 'revision',
- ],
- 'fields' => array_merge(
- [
- 'slot_revision_id' => 'slots.rev_id',
- 'slot_content_id' => 'NULL',
- 'slot_origin' => 'slots.rev_id',
- 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
- 'content_size' => 'slots.rev_len',
- 'content_sha1' => 'slots.rev_sha1',
- 'content_address' =>
- $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
- 'model_name' => 'NULL',
- ]
- ),
- 'joins' => [],
- ]
- ];
- }
-
- public function provideNewMutableRevisionFromArray() {
- foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
- yield $case;
- }
-
- yield 'Basic array, with page & id' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'text_id' => 2,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- ]
- ];
- }
-
-}
use CommentStoreComment;
use Content;
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
use MediaWikiTestCase;
use ParserOptions;
use RecentChange;
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use Revision;
-use WikitextContent;
-
-/**
- * Tests RevisionStore against the pre-MCR DB schema.
- *
- * @covers \MediaWiki\Storage\RevisionStore
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- * @group medium
- */
-class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
-
- use PreMcrSchemaOverride;
-
- protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
- $row = parent::revisionToRow( $rev, $options );
-
- $row->rev_text_id = (string)$rev->getTextId();
- $row->rev_content_format = (string)$rev->getContentFormat();
- $row->rev_content_model = (string)$rev->getContentModel();
-
- return $row;
- }
-
- protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
- // Legacy schema is still being written
- $this->assertSelect(
- [ 'revision', 'text' ],
- [ 'count(*)' ],
- [ 'rev_id' => $rev->getId(), 'rev_text_id > 0' ],
- [ [ 1 ] ],
- [],
- [ 'text' => [ 'INNER JOIN', [ 'rev_text_id = old_id' ] ] ]
- );
-
- parent::assertRevisionExistsInDatabase( $rev );
- }
-
- public function provideInsertRevisionOn_failures() {
- foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
- yield $case;
- }
-
- yield 'slot that is not main slot' => [
- [
- 'content' => [
- 'main' => new WikitextContent( 'Chicken' ),
- 'lalala' => new WikitextContent( 'Duck' ),
- ],
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- new InvalidArgumentException( 'Only the main slot is supported' )
- ];
- }
-
- public function provideNewMutableRevisionFromArray() {
- foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
- yield $case;
- }
-
- yield 'Basic array, with page & id' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'text_id' => 2,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'content_format' => 'text/x-wiki',
- 'content_model' => 'wikitext',
- ]
- ];
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use Wikimedia\Rdbms\IMaintainableDatabase;
-use MediaWiki\DB\PatchFileLocation;
-
-/**
- * Trait providing schema overrides that allow tests to run against the pre-MCR database schema.
- */
-trait PreMcrSchemaOverride {
-
- use PatchFileLocation;
- use McrSchemaDetection;
-
- /**
- * @return int
- */
- protected function getMcrMigrationStage() {
- return MIGRATION_OLD;
- }
-
- /**
- * @return string[]
- */
- protected function getMcrTablesToReset() {
- return [];
- }
-
- /**
- * @return array[]
- */
- protected function getSchemaOverrides( IMaintainableDatabase $db ) {
- $overrides = [
- 'scripts' => [],
- 'drop' => [],
- 'create' => [],
- 'alter' => [],
- ];
-
- if ( $this->hasMcrTables( $db ) ) {
- $overrides['drop'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/drop-mcr-tables', __DIR__ );
- }
-
- if ( !$this->hasPreMcrFields( $db ) ) {
- $overrides['alter'][] = 'revision';
- $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/create-pre-mcr-fields', __DIR__ );
- }
-
- return $overrides;
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\RevisionArchiveRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use TextContent;
-use Title;
-
-/**
- * @covers \MediaWiki\Storage\RevisionArchiveRecord
- * @covers \MediaWiki\Storage\RevisionRecord
- */
-class RevisionArchiveRecordTest extends MediaWikiTestCase {
-
- use RevisionRecordTests;
-
- /**
- * @param array $rowOverrides
- *
- * @return RevisionArchiveRecord
- */
- protected function newRevision( array $rowOverrides = [] ) {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- $user = new UserIdentityValue( 11, 'Tester', 0 );
- $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
- $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
- $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
- $slots = new RevisionSlots( [ $main, $aux ] );
-
- $row = [
- 'ar_id' => '5',
- 'ar_rev_id' => '7',
- 'ar_page_id' => strval( $title->getArticleID() ),
- 'ar_timestamp' => '20200101000000',
- 'ar_deleted' => 0,
- 'ar_minor_edit' => 0,
- 'ar_parent_id' => '5',
- 'ar_len' => $slots->computeSize(),
- 'ar_sha1' => $slots->computeSha1(),
- ];
-
- foreach ( $rowOverrides as $field => $value ) {
- $field = preg_replace( '/^rev_/', 'ar_', $field );
- $row[$field] = $value;
- }
-
- return new RevisionArchiveRecord( $title, $user, $comment, (object)$row, $slots );
- }
-
- public function provideConstructor() {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- $user = new UserIdentityValue( 11, 'Tester', 0 );
- $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
- $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
- $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
- $slots = new RevisionSlots( [ $main, $aux ] );
-
- $protoRow = [
- 'ar_id' => '5',
- 'ar_rev_id' => '7',
- 'ar_page_id' => strval( $title->getArticleID() ),
- 'ar_timestamp' => '20200101000000',
- 'ar_deleted' => 0,
- 'ar_minor_edit' => 0,
- 'ar_parent_id' => '5',
- 'ar_len' => $slots->computeSize(),
- 'ar_sha1' => $slots->computeSha1(),
- ];
-
- $row = $protoRow;
- yield 'all info' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots,
- 'acmewiki'
- ];
-
- $row = $protoRow;
- $row['ar_minor_edit'] = '1';
- $row['ar_deleted'] = strval( RevisionRecord::DELETED_USER );
-
- yield 'minor deleted' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- unset( $row['ar_parent'] );
-
- yield 'no parent' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- $row['ar_len'] = null;
- $row['ar_sha1'] = '';
-
- yield 'ar_len is null, ar_sha1 is ""' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- yield 'no length, no hash' => [
- Title::newFromText( 'DummyDoesNotExist' ),
- $user,
- $comment,
- (object)$row,
- $slots
- ];
- }
-
- /**
- * @dataProvider provideConstructor
- *
- * @param Title $title
- * @param UserIdentity $user
- * @param CommentStoreComment $comment
- * @param object $row
- * @param RevisionSlots $slots
- * @param bool $wikiId
- */
- public function testConstructorAndGetters(
- Title $title,
- UserIdentity $user,
- CommentStoreComment $comment,
- $row,
- RevisionSlots $slots,
- $wikiId = false
- ) {
- $rec = new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
-
- $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
- $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
- $this->assertSame( $comment, $rec->getComment(), 'getComment' );
-
- $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
- $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
-
- $this->assertSame( (int)$row->ar_id, $rec->getArchiveId(), 'getArchiveId' );
- $this->assertSame( (int)$row->ar_rev_id, $rec->getId(), 'getId' );
- $this->assertSame( (int)$row->ar_page_id, $rec->getPageId(), 'getId' );
- $this->assertSame( $row->ar_timestamp, $rec->getTimestamp(), 'getTimestamp' );
- $this->assertSame( (int)$row->ar_deleted, $rec->getVisibility(), 'getVisibility' );
- $this->assertSame( (bool)$row->ar_minor_edit, $rec->isMinor(), 'getIsMinor' );
-
- if ( isset( $row->ar_parent_id ) ) {
- $this->assertSame( (int)$row->ar_parent_id, $rec->getParentId(), 'getParentId' );
- } else {
- $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
- }
-
- if ( isset( $row->ar_len ) ) {
- $this->assertSame( (int)$row->ar_len, $rec->getSize(), 'getSize' );
- } else {
- $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
- }
-
- if ( !empty( $row->ar_sha1 ) ) {
- $this->assertSame( $row->ar_sha1, $rec->getSha1(), 'getSha1' );
- } else {
- $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
- }
- }
-
- public function provideConstructorFailure() {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- $user = new UserIdentityValue( 11, 'Tester', 0 );
-
- $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
- $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
- $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
- $slots = new RevisionSlots( [ $main, $aux ] );
-
- $protoRow = [
- 'ar_id' => '5',
- 'ar_rev_id' => '7',
- 'ar_page_id' => strval( $title->getArticleID() ),
- 'ar_timestamp' => '20200101000000',
- 'ar_deleted' => 0,
- 'ar_minor_edit' => 0,
- 'ar_parent_id' => '5',
- 'ar_len' => $slots->computeSize(),
- 'ar_sha1' => $slots->computeSha1(),
- ];
-
- yield 'not a row' => [
- $title,
- $user,
- $comment,
- 'not a row',
- $slots,
- 'acmewiki'
- ];
-
- $row = $protoRow;
- $row['ar_timestamp'] = 'kittens';
-
- yield 'bad timestamp' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
-
- yield 'bad wiki' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots,
- 12345
- ];
-
- // NOTE: $title->getArticleID does *not* have to match ar_page_id in all cases!
- }
-
- /**
- * @dataProvider provideConstructorFailure
- *
- * @param Title $title
- * @param UserIdentity $user
- * @param CommentStoreComment $comment
- * @param object $row
- * @param RevisionSlots $slots
- * @param bool $wikiId
- */
- public function testConstructorFailure(
- Title $title,
- UserIdentity $user,
- CommentStoreComment $comment,
- $row,
- RevisionSlots $slots,
- $wikiId = false
- ) {
- $this->setExpectedException( InvalidArgumentException::class );
- new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $wikiId );
- }
-
-}
+++ /dev/null
-<?php
-namespace MediaWiki\Tests\Storage;
-
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
-use MediaWikiTestCase;
-use Revision;
-
-/**
- * Tests RevisionStore against the post-migration MCR DB schema.
- *
- * @group RevisionStore
- * @group Storage
- * @group Database
- */
-class RevisionQueryInfoTest extends MediaWikiTestCase {
-
- protected function getRevisionQueryFields( $returnTextIdField = true ) {
- $fields = [
- 'rev_id',
- 'rev_page',
- 'rev_timestamp',
- 'rev_minor_edit',
- 'rev_deleted',
- 'rev_len',
- 'rev_parent_id',
- 'rev_sha1',
- ];
- if ( $returnTextIdField ) {
- $fields[] = 'rev_text_id';
- }
- return $fields;
- }
-
- protected function getArchiveQueryFields( $returnTextFields = true ) {
- $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',
- ];
- if ( $returnTextFields ) {
- $fields[] = 'ar_text_id';
- }
- return $fields;
- }
-
- protected function getOldCommentQueryFields( $prefix ) {
- return [
- "{$prefix}_comment_text" => "{$prefix}_comment",
- "{$prefix}_comment_data" => 'NULL',
- "{$prefix}_comment_cid" => 'NULL',
- ];
- }
-
- protected function getCompatCommentQueryFields( $prefix ) {
- return [
- "{$prefix}_comment_text"
- => "COALESCE( comment_{$prefix}_comment.comment_text, {$prefix}_comment )",
- "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
- "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
- ];
- }
-
- protected function getNewCommentQueryFields( $prefix ) {
- return [
- "{$prefix}_comment_text" => "comment_{$prefix}_comment.comment_text",
- "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
- "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
- ];
- }
-
- protected function getOldActorQueryFields( $prefix ) {
- return [
- "{$prefix}_user" => "{$prefix}_user",
- "{$prefix}_user_text" => "{$prefix}_user_text",
- "{$prefix}_actor" => 'NULL',
- ];
- }
-
- protected function getNewActorQueryFields( $prefix, $tmp = false ) {
- return [
- "{$prefix}_user" => "actor_{$prefix}_user.actor_user",
- "{$prefix}_user_text" => "actor_{$prefix}_user.actor_name",
- "{$prefix}_actor" => $tmp ?: "{$prefix}_actor",
- ];
- }
-
- protected function getCompatActorQueryFields( $prefix, $tmp = false ) {
- return [
- "{$prefix}_user" => "COALESCE( actor_{$prefix}_user.actor_user, {$prefix}_user )",
- "{$prefix}_user_text" => "COALESCE( actor_{$prefix}_user.actor_name, {$prefix}_user_text )",
- "{$prefix}_actor" => $tmp ?: "{$prefix}_actor",
- ];
- }
-
- protected function getCompatActorJoins( $prefix ) {
- return [
- "temp_{$prefix}_user" => [
- "LEFT JOIN",
- "temp_{$prefix}_user.revactor_{$prefix} = {$prefix}_id",
- ],
- "actor_{$prefix}_user" => [
- "LEFT JOIN",
- "actor_{$prefix}_user.actor_id = temp_{$prefix}_user.revactor_actor",
- ],
- ];
- }
-
- protected function getCompatCommentJoins( $prefix ) {
- return [
- "temp_{$prefix}_comment" => [
- "LEFT JOIN",
- "temp_{$prefix}_comment.revcomment_{$prefix} = {$prefix}_id",
- ],
- "comment_{$prefix}_comment" => [
- "LEFT JOIN",
- "comment_{$prefix}_comment.comment_id = temp_{$prefix}_comment.revcomment_comment_id",
- ],
- ];
- }
-
- protected function getTextQueryFields() {
- return [
- 'old_text',
- 'old_flags',
- ];
- }
-
- protected function getPageQueryFields() {
- return [
- 'page_namespace',
- 'page_title',
- 'page_id',
- 'page_latest',
- 'page_is_redirect',
- 'page_len',
- ];
- }
-
- protected function getUserQueryFields() {
- return [
- 'user_name',
- ];
- }
-
- protected function getContentHandlerQueryFields( $prefix ) {
- return [
- "{$prefix}_content_format",
- "{$prefix}_content_model",
- ];
- }
-
- public function provideArchiveQueryInfo() {
- yield 'MCR, comment, actor' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_NEW,
- ],
- [
- 'tables' => [
- 'archive',
- 'actor_ar_user' => 'actor',
- 'comment_ar_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getArchiveQueryFields( false ),
- $this->getNewActorQueryFields( 'ar' ),
- $this->getNewCommentQueryFields( 'ar' )
- ),
- 'joins' => [
- 'comment_ar_comment'
- => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
- 'actor_ar_user' => [ 'JOIN', 'actor_ar_user.actor_id = ar_actor' ],
- ],
- ]
- ];
- yield 'read-new MCR, comment, actor' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
- ],
- [
- 'tables' => [
- 'archive',
- 'actor_ar_user' => 'actor',
- 'comment_ar_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getArchiveQueryFields( false ),
- $this->getCompatActorQueryFields( 'ar' ),
- $this->getCompatCommentQueryFields( 'ar' )
- ),
- 'joins' => [
- 'comment_ar_comment'
- => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
- 'actor_ar_user' => [ 'LEFT JOIN', 'actor_ar_user.actor_id = ar_actor' ],
- ],
- ]
- ];
- yield 'MCR write-both/read-old' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- ],
- [
- 'tables' => [
- 'archive',
- 'actor_ar_user' => 'actor',
- 'comment_ar_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getArchiveQueryFields( true ),
- $this->getContentHandlerQueryFields( 'ar' ),
- $this->getCompatActorQueryFields( 'ar' ),
- $this->getCompatCommentQueryFields( 'ar' )
- ),
- 'joins' => [
- 'comment_ar_comment'
- => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
- 'actor_ar_user' => [ 'LEFT JOIN', 'actor_ar_user.actor_id = ar_actor' ],
- ],
- ]
- ];
- yield 'pre-MCR, no model' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [
- 'tables' => [
- 'archive',
- ],
- 'fields' => array_merge(
- $this->getArchiveQueryFields( true ),
- $this->getOldActorQueryFields( 'ar' ),
- $this->getOldCommentQueryFields( 'ar' )
- ),
- 'joins' => [],
- ]
- ];
- }
-
- public function provideQueryInfo() {
- // TODO: more option variations
- yield 'MCR, page, user, comment, actor' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_NEW,
- ],
- [ 'page', 'user' ],
- [
- 'tables' => [
- 'revision',
- 'page',
- 'user',
- 'temp_rev_user' => 'revision_actor_temp',
- 'temp_rev_comment' => 'revision_comment_temp',
- 'actor_rev_user' => 'actor',
- 'comment_rev_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( false ),
- $this->getPageQueryFields(),
- $this->getUserQueryFields(),
- $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
- $this->getNewCommentQueryFields( 'rev' )
- ),
- 'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
- 'user' => [
- 'LEFT JOIN',
- [ 'actor_rev_user.actor_user != 0', 'user_id = actor_rev_user.actor_user' ],
- ],
- 'comment_rev_comment' => [
- 'JOIN',
- 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id',
- ],
- 'actor_rev_user' => [
- 'JOIN',
- 'actor_rev_user.actor_id = temp_rev_user.revactor_actor',
- ],
- 'temp_rev_user' => [ 'JOIN', 'temp_rev_user.revactor_rev = rev_id' ],
- 'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
- ],
- ]
- ];
- yield 'MCR read-new, page, user, comment, actor' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
- ],
- [ 'page', 'user' ],
- [
- 'tables' => [
- 'revision',
- 'page',
- 'user',
- 'temp_rev_user' => 'revision_actor_temp',
- 'temp_rev_comment' => 'revision_comment_temp',
- 'actor_rev_user' => 'actor',
- 'comment_rev_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( false ),
- $this->getPageQueryFields(),
- $this->getUserQueryFields(),
- $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
- $this->getCompatCommentQueryFields( 'rev' )
- ),
- 'joins' => array_merge(
- [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
- 'user' => [
- 'LEFT JOIN',
- [
- 'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
- 'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
- ]
- ],
- ],
- $this->getCompatActorJoins( 'rev' ),
- $this->getCompatCommentJoins( 'rev' )
- ),
- ]
- ];
- yield 'MCR read-new' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
- ],
- [ 'page', 'user' ],
- [
- 'tables' => [
- 'revision',
- 'page',
- 'user',
- 'temp_rev_user' => 'revision_actor_temp',
- 'temp_rev_comment' => 'revision_comment_temp',
- 'actor_rev_user' => 'actor',
- 'comment_rev_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( false ),
- $this->getPageQueryFields(),
- $this->getUserQueryFields(),
- $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
- $this->getCompatCommentQueryFields( 'rev' )
- ),
- 'joins' => array_merge(
- [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
- 'user' => [
- 'LEFT JOIN',
- [
- 'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
- 'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
- ]
- ],
- ],
- $this->getCompatActorJoins( 'rev' ),
- $this->getCompatCommentJoins( 'rev' )
- ),
- ]
- ];
- yield 'MCR write-both/read-old' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- ],
- [],
- [
- 'tables' => [
- 'revision',
- 'temp_rev_user' => 'revision_actor_temp',
- 'temp_rev_comment' => 'revision_comment_temp',
- 'actor_rev_user' => 'actor',
- 'comment_rev_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getContentHandlerQueryFields( 'rev' ),
- $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
- $this->getCompatCommentQueryFields( 'rev' )
- ),
- 'joins' => array_merge(
- $this->getCompatActorJoins( 'rev' ),
- $this->getCompatCommentJoins( 'rev' )
- ),
- ]
- ];
- yield 'MCR write-both/read-old, page, user' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- ],
- [ 'page', 'user' ],
- [
- 'tables' => [
- 'revision',
- 'page',
- 'user',
- 'temp_rev_user' => 'revision_actor_temp',
- 'temp_rev_comment' => 'revision_comment_temp',
- 'actor_rev_user' => 'actor',
- 'comment_rev_comment' => 'comment',
- ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getContentHandlerQueryFields( 'rev' ),
- $this->getUserQueryFields(),
- $this->getPageQueryFields(),
- $this->getCompatActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
- $this->getCompatCommentQueryFields( 'rev' )
- ),
- 'joins' => array_merge(
- [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
- 'user' => [
- 'LEFT JOIN',
- [
- 'COALESCE( actor_rev_user.actor_user, rev_user ) != 0',
- 'user_id = COALESCE( actor_rev_user.actor_user, rev_user )'
- ]
- ],
- ],
- $this->getCompatActorJoins( 'rev' ),
- $this->getCompatCommentJoins( 'rev' )
- ),
- ]
- ];
- yield 'pre-MCR' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [],
- [
- 'tables' => [ 'revision' ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getContentHandlerQueryFields( 'rev' ),
- $this->getOldActorQueryFields( 'rev' ),
- $this->getOldCommentQueryFields( 'rev' )
- ),
- 'joins' => [],
- ]
- ];
- yield 'pre-MCR, page, user' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [ 'page', 'user' ],
- [
- 'tables' => [ 'revision', 'page', 'user' ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getContentHandlerQueryFields( 'rev' ),
- $this->getPageQueryFields(),
- $this->getUserQueryFields(),
- $this->getOldActorQueryFields( 'rev' ),
- $this->getOldCommentQueryFields( 'rev' )
- ),
- 'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
- 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
- ],
- ]
- ];
- yield 'pre-MCR, no model' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [],
- [
- 'tables' => [ 'revision' ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getOldActorQueryFields( 'rev' ),
- $this->getOldCommentQueryFields( 'rev' )
- ),
- 'joins' => [],
- ],
- ];
- yield 'pre-MCR, no model, page' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [ 'page' ],
- [
- 'tables' => [ 'revision', 'page' ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getPageQueryFields(),
- $this->getOldActorQueryFields( 'rev' ),
- $this->getOldCommentQueryFields( 'rev' )
- ),
- 'joins' => [
- 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ], ],
- ],
- ],
- ];
- yield 'pre-MCR, no model, user' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [ 'user' ],
- [
- 'tables' => [ 'revision', 'user' ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getUserQueryFields(),
- $this->getOldActorQueryFields( 'rev' ),
- $this->getOldCommentQueryFields( 'rev' )
- ),
- 'joins' => [
- 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
- ],
- ],
- ];
- yield 'pre-MCR, no model, text' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [ 'text' ],
- [
- 'tables' => [ 'revision', 'text' ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getTextQueryFields(),
- $this->getOldActorQueryFields( 'rev' ),
- $this->getOldCommentQueryFields( 'rev' )
- ),
- 'joins' => [
- 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
- ],
- ],
- ];
- yield 'pre-MCR, no model, text, page, user' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- [ 'text', 'page', 'user' ],
- [
- 'tables' => [
- 'revision', 'page', 'user', 'text'
- ],
- 'fields' => array_merge(
- $this->getRevisionQueryFields( true ),
- $this->getPageQueryFields(),
- $this->getUserQueryFields(),
- $this->getTextQueryFields(),
- $this->getOldActorQueryFields( 'rev' ),
- $this->getOldCommentQueryFields( 'rev' )
- ),
- 'joins' => [
- 'page' => [
- 'INNER JOIN',
- [ 'page_id = rev_page' ],
- ],
- 'user' => [
- 'LEFT JOIN',
- [
- 'rev_user != 0',
- 'user_id = rev_user',
- ],
- ],
- 'text' => [
- 'INNER JOIN',
- [ 'rev_text_id=old_id' ],
- ],
- ],
- ],
- ];
- }
-
- public function provideSlotsQueryInfo() {
- yield 'MCR, no options' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
- ],
- [],
- [
- 'tables' => [
- 'slots'
- ],
- 'fields' => [
- 'slot_revision_id',
- 'slot_content_id',
- 'slot_origin',
- 'slot_role_id',
- ],
- 'joins' => [],
- ]
- ];
- yield 'MCR, role option' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
- ],
- [ 'role' ],
- [
- 'tables' => [
- 'slots',
- 'slot_roles',
- ],
- 'fields' => [
- 'slot_revision_id',
- 'slot_content_id',
- 'slot_origin',
- 'slot_role_id',
- 'role_name',
- ],
- 'joins' => [
- 'slot_roles' => [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ],
- ],
- ]
- ];
- yield 'MCR read-new, content option' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
- ],
- [ 'content' ],
- [
- 'tables' => [
- 'slots',
- 'content',
- ],
- 'fields' => [
- 'slot_revision_id',
- 'slot_content_id',
- 'slot_origin',
- 'slot_role_id',
- 'content_size',
- 'content_sha1',
- 'content_address',
- 'content_model',
- ],
- 'joins' => [
- 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
- ],
- ]
- ];
- yield 'MCR read-new, content and model options' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
- ],
- [ 'content', 'model' ],
- [
- 'tables' => [
- 'slots',
- 'content',
- 'content_models',
- ],
- 'fields' => [
- 'slot_revision_id',
- 'slot_content_id',
- 'slot_origin',
- 'slot_role_id',
- 'content_size',
- 'content_sha1',
- 'content_address',
- 'content_model',
- 'model_name',
- ],
- 'joins' => [
- 'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
- 'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ],
- ],
- ]
- ];
-
- $db = wfGetDB( DB_REPLICA );
-
- yield 'MCR write-both/read-old' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
- ],
- [],
- [
- 'tables' => [
- 'slots' => 'revision',
- ],
- 'fields' => array_merge(
- [
- 'slot_revision_id' => 'slots.rev_id',
- 'slot_content_id' => 'NULL',
- 'slot_origin' => 'slots.rev_id',
- 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
- ]
- ),
- 'joins' => [],
- ]
- ];
- yield 'MCR write-both/read-old, content' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
- ],
- [ 'content' ],
- [
- 'tables' => [
- 'slots' => 'revision',
- ],
- 'fields' => array_merge(
- [
- 'slot_revision_id' => 'slots.rev_id',
- 'slot_content_id' => 'NULL',
- 'slot_origin' => 'slots.rev_id',
- 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
- 'content_size' => 'slots.rev_len',
- 'content_sha1' => 'slots.rev_sha1',
- 'content_address' => $db->buildConcat( [
- $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
- 'model_name' => 'slots.rev_content_model',
- ]
- ),
- 'joins' => [],
- ]
- ];
- yield 'MCR write-both/read-old, content, model, role' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
- ],
- [ 'content', 'model', 'role' ],
- [
- 'tables' => [
- 'slots' => 'revision',
- ],
- 'fields' => array_merge(
- [
- 'slot_revision_id' => 'slots.rev_id',
- 'slot_content_id' => 'NULL',
- 'slot_origin' => 'slots.rev_id',
- 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
- 'content_size' => 'slots.rev_len',
- 'content_sha1' => 'slots.rev_sha1',
- 'content_address' => $db->buildConcat( [
- $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
- 'model_name' => 'slots.rev_content_model',
- ]
- ),
- 'joins' => [],
- ]
- ];
- yield 'pre-MCR' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_OLD,
- ],
- [],
- [
- 'tables' => [
- 'slots' => 'revision',
- ],
- 'fields' => array_merge(
- [
- 'slot_revision_id' => 'slots.rev_id',
- 'slot_content_id' => 'NULL',
- 'slot_origin' => 'slots.rev_id',
- 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
- ]
- ),
- 'joins' => [],
- ]
- ];
- yield 'pre-MCR, content' => [
- [
- 'wgMultiContentRevisionSchemaMigrationStage'
- => SCHEMA_COMPAT_OLD,
- ],
- [ 'content' ],
- [
- 'tables' => [
- 'slots' => 'revision',
- ],
- 'fields' => array_merge(
- [
- 'slot_revision_id' => 'slots.rev_id',
- 'slot_content_id' => 'NULL',
- 'slot_origin' => 'slots.rev_id',
- 'role_name' => $db->addQuotes( SlotRecord::MAIN ),
- 'content_size' => 'slots.rev_len',
- 'content_sha1' => 'slots.rev_sha1',
- 'content_address' =>
- $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] ),
- 'model_name' => 'slots.rev_content_model',
- ]
- ),
- 'joins' => [],
- ]
- ];
- }
-
- public function provideSelectFields() {
- yield 'with model, comment, and actor' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- ],
- 'fields' => array_merge(
- [
- 'rev_id',
- 'rev_page',
- 'rev_text_id',
- 'rev_timestamp',
- 'rev_user_text',
- 'rev_user',
- 'rev_actor' => 'NULL',
- 'rev_minor_edit',
- 'rev_deleted',
- 'rev_len',
- 'rev_parent_id',
- 'rev_sha1',
- ],
- $this->getContentHandlerQueryFields( 'rev' ),
- [
- 'rev_comment_old' => 'rev_comment',
- 'rev_comment_pk' => 'rev_id',
- ]
- ),
- ];
- yield 'no mode, no comment, no actor' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- 'fields' => array_merge(
- [
- 'rev_id',
- 'rev_page',
- 'rev_text_id',
- 'rev_timestamp',
- 'rev_user_text',
- 'rev_user',
- 'rev_actor' => 'NULL',
- 'rev_minor_edit',
- 'rev_deleted',
- 'rev_len',
- 'rev_parent_id',
- 'rev_sha1',
- ],
- $this->getOldCommentQueryFields( 'rev' )
- ),
- ];
- }
-
- public function provideSelectArchiveFields() {
- yield 'with model, comment, and actor' => [
- [
- 'wgContentHandlerUseDB' => true,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
- ],
- 'fields' => array_merge(
- [
- 'ar_id',
- 'ar_page_id',
- 'ar_rev_id',
- 'ar_text_id',
- 'ar_timestamp',
- 'ar_user_text',
- 'ar_user',
- 'ar_actor' => 'NULL',
- 'ar_minor_edit',
- 'ar_deleted',
- 'ar_len',
- 'ar_parent_id',
- 'ar_sha1',
- ],
- $this->getContentHandlerQueryFields( 'ar' ),
- [
- 'ar_comment_old' => 'ar_comment',
- 'ar_comment_id' => 'ar_comment_id',
- ]
- ),
- ];
- yield 'no mode, no comment, no actor' => [
- [
- 'wgContentHandlerUseDB' => false,
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ],
- 'fields' => array_merge(
- [
- 'ar_id',
- 'ar_page_id',
- 'ar_rev_id',
- 'ar_text_id',
- 'ar_timestamp',
- 'ar_user_text',
- 'ar_user',
- 'ar_actor' => 'NULL',
- 'ar_minor_edit',
- 'ar_deleted',
- 'ar_len',
- 'ar_parent_id',
- 'ar_sha1',
- ],
- $this->getOldCommentQueryFields( 'ar' )
- ),
- ];
- }
-
- /**
- * @dataProvider provideSelectFields
- * @covers Revision::selectFields
- */
- public function testRevisionSelectFields( $migrationStageSettings, $expected ) {
- $this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
-
- $this->hideDeprecated( 'Revision::selectFields' );
- $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectFields() );
- }
-
- /**
- * @dataProvider provideSelectArchiveFields
- * @covers Revision::selectArchiveFields
- */
- public function testRevisionSelectArchiveFields( $migrationStageSettings, $expected ) {
- $this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
-
- $this->hideDeprecated( 'Revision::selectArchiveFields' );
- $this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectArchiveFields() );
- }
-
- /**
- * @covers Revision::userJoinCond
- */
- public function testRevisionUserJoinCond() {
- $this->hideDeprecated( 'Revision::userJoinCond' );
- $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
- $this->overrideMwServices();
- $this->assertEquals(
- [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
- Revision::userJoinCond()
- );
- }
-
- /**
- * @covers Revision::pageJoinCond
- */
- public function testRevisionPageJoinCond() {
- $this->hideDeprecated( 'Revision::pageJoinCond' );
- $this->assertEquals(
- [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
- Revision::pageJoinCond()
- );
- }
-
- /**
- * @covers Revision::selectTextFields
- */
- public function testRevisionSelectTextFields() {
- $this->hideDeprecated( 'Revision::selectTextFields' );
- $this->assertEquals(
- $this->getTextQueryFields(),
- Revision::selectTextFields()
- );
- }
-
- /**
- * @covers Revision::selectPageFields
- */
- public function testRevisionSelectPageFields() {
- $this->hideDeprecated( 'Revision::selectPageFields' );
- $this->assertEquals(
- $this->getPageQueryFields(),
- Revision::selectPageFields()
- );
- }
-
- /**
- * @covers Revision::selectUserFields
- */
- public function testRevisionSelectUserFields() {
- $this->hideDeprecated( 'Revision::selectUserFields' );
- $this->assertEquals(
- $this->getUserQueryFields(),
- Revision::selectUserFields()
- );
- }
-
- /**
- * @covers Revision::getArchiveQueryInfo
- * @dataProvider provideArchiveQueryInfo
- */
- public function testRevisionGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
- $this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
-
- $queryInfo = Revision::getArchiveQueryInfo();
- $this->assertQueryInfoEquals( $expected, $queryInfo );
- }
-
- /**
- * @covers Revision::getQueryInfo
- * @dataProvider provideQueryInfo
- */
- public function testRevisionGetQueryInfo( $migrationStageSettings, $options, $expected ) {
- $this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
-
- $queryInfo = Revision::getQueryInfo( $options );
- $this->assertQueryInfoEquals( $expected, $queryInfo );
- }
-
- /**
- * @dataProvider provideQueryInfo
- * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
- */
- public function testRevisionStoreGetQueryInfo( $migrationStageSettings, $options, $expected ) {
- $this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- $queryInfo = $store->getQueryInfo( $options );
- $this->assertQueryInfoEquals( $expected, $queryInfo );
- }
-
- /**
- * @dataProvider provideSlotsQueryInfo
- * @covers \MediaWiki\Storage\RevisionStore::getSlotsQueryInfo
- */
- public function testRevisionStoreGetSlotsQueryInfo(
- $migrationStageSettings,
- $options,
- $expected
- ) {
- $this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- $queryInfo = $store->getSlotsQueryInfo( $options );
- $this->assertQueryInfoEquals( $expected, $queryInfo );
- }
-
- /**
- * @dataProvider provideArchiveQueryInfo
- * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
- */
- public function testRevisionStoreGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
- $this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- $queryInfo = $store->getArchiveQueryInfo();
- $this->assertQueryInfoEquals( $expected, $queryInfo );
- }
-
- private function assertQueryInfoEquals( $expected, $queryInfo ) {
- $this->assertArrayEqualsIgnoringIntKeyOrder(
- $expected['tables'],
- $queryInfo['tables'],
- 'tables'
- );
- $this->assertArrayEqualsIgnoringIntKeyOrder(
- $expected['fields'],
- $queryInfo['fields'],
- 'fields'
- );
- $this->assertArrayEqualsIgnoringIntKeyOrder(
- $expected['joins'],
- $queryInfo['joins'],
- 'joins'
- );
- }
-
- /**
- * Assert that the two arrays passed are equal, ignoring the order of the values that integer
- * keys.
- *
- * Note: Failures of this assertion can be slightly confusing as the arrays are actually
- * split into a string key array and an int key array before assertions occur.
- *
- * @param array $expected
- * @param array $actual
- */
- private function assertArrayEqualsIgnoringIntKeyOrder(
- array $expected,
- array $actual,
- $message = null
- ) {
- $this->objectAssociativeSort( $expected );
- $this->objectAssociativeSort( $actual );
-
- // Separate the int key values from the string key values so that assertion failures are
- // easier to understand.
- $expectedIntKeyValues = [];
- $actualIntKeyValues = [];
-
- // Remove all int keys and re add them at the end after sorting by value
- // This will result in all int keys being in the same order with same ints at the end of
- // the array
- foreach ( $expected as $key => $value ) {
- if ( is_int( $key ) ) {
- unset( $expected[$key] );
- $expectedIntKeyValues[] = $value;
- }
- }
- foreach ( $actual as $key => $value ) {
- if ( is_int( $key ) ) {
- unset( $actual[$key] );
- $actualIntKeyValues[] = $value;
- }
- }
-
- $this->objectAssociativeSort( $expected );
- $this->objectAssociativeSort( $actual );
-
- $this->objectAssociativeSort( $expectedIntKeyValues );
- $this->objectAssociativeSort( $actualIntKeyValues );
-
- $this->assertEquals( $expected, $actual, $message );
- $this->assertEquals( $expectedIntKeyValues, $actualIntKeyValues, $message );
- }
-
-}
+++ /dev/null
-<?php
-
-// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClassTrait
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use LogicException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SuppressedDataException;
-use MediaWiki\User\UserIdentityValue;
-use TextContent;
-use Title;
-
-/**
- * @covers \MediaWiki\Storage\RevisionRecord
- *
- * @note Expects to be used in classes that extend MediaWikiTestCase.
- */
-trait RevisionRecordTests {
-
- /**
- * @param array $rowOverrides
- *
- * @return RevisionRecord
- */
- protected abstract function newRevision( array $rowOverrides = [] );
-
- private function provideAudienceCheckData( $field ) {
- yield 'field accessible for oversighter (ALL)' => [
- RevisionRecord::SUPPRESSED_ALL,
- [ 'oversight' ],
- true,
- false
- ];
-
- yield 'field accessible for oversighter' => [
- RevisionRecord::DELETED_RESTRICTED | $field,
- [ 'oversight' ],
- true,
- false
- ];
-
- yield 'field not accessible for sysops (ALL)' => [
- RevisionRecord::SUPPRESSED_ALL,
- [ 'sysop' ],
- false,
- false
- ];
-
- yield 'field not accessible for sysops' => [
- RevisionRecord::DELETED_RESTRICTED | $field,
- [ 'sysop' ],
- false,
- false
- ];
-
- yield 'field accessible for sysops' => [
- $field,
- [ 'sysop' ],
- true,
- false
- ];
-
- yield 'field suppressed for logged in users' => [
- $field,
- [ 'user' ],
- false,
- false
- ];
-
- yield 'unrelated field suppressed' => [
- $field === RevisionRecord::DELETED_COMMENT
- ? RevisionRecord::DELETED_USER
- : RevisionRecord::DELETED_COMMENT,
- [ 'user' ],
- true,
- true
- ];
-
- yield 'nothing suppressed' => [
- 0,
- [ 'user' ],
- true,
- true
- ];
- }
-
- public function testSerialization_fails() {
- $this->setExpectedException( LogicException::class );
- $rev = $this->newRevision();
- serialize( $rev );
- }
-
- public function provideGetComment_audience() {
- return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
- }
-
- private function forceStandardPermissions() {
- $this->setMwGlobals(
- 'wgGroupPermissions',
- [
- 'user' => [
- 'viewsuppressed' => false,
- 'suppressrevision' => false,
- 'deletedtext' => false,
- 'deletedhistory' => false,
- ],
- 'sysop' => [
- 'viewsuppressed' => false,
- 'suppressrevision' => false,
- 'deletedtext' => true,
- 'deletedhistory' => true,
- ],
- 'oversight' => [
- 'deletedtext' => true,
- 'deletedhistory' => true,
- 'viewsuppressed' => true,
- 'suppressrevision' => true,
- ],
- ]
- );
- }
-
- /**
- * @dataProvider provideGetComment_audience
- */
- public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
- $this->forceStandardPermissions();
-
- $user = $this->getTestUser( $groups )->getUser();
- $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
- $this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
-
- $this->assertSame(
- $publicCan,
- $rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
- 'public can'
- );
- $this->assertSame(
- $userCan,
- $rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
- 'user can'
- );
- }
-
- public function provideGetUser_audience() {
- return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
- }
-
- /**
- * @dataProvider provideGetUser_audience
- */
- public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
- $this->forceStandardPermissions();
-
- $user = $this->getTestUser( $groups )->getUser();
- $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
- $this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
-
- $this->assertSame(
- $publicCan,
- $rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
- 'public can'
- );
- $this->assertSame(
- $userCan,
- $rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
- 'user can'
- );
- }
-
- public function provideGetSlot_audience() {
- return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
- }
-
- /**
- * @dataProvider provideGetSlot_audience
- */
- public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
- $this->forceStandardPermissions();
-
- $user = $this->getTestUser( $groups )->getUser();
- $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
- // NOTE: slot meta-data is never suppressed, just the content is!
- $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ), 'hasSlot is never suppressed' );
- $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw meta' );
- $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
- 'public meta' );
-
- $this->assertNotNull(
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
- 'user can'
- );
-
- try {
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
- $exception = null;
- } catch ( SuppressedDataException $ex ) {
- $exception = $ex;
- }
-
- $this->assertSame(
- $publicCan,
- $exception === null,
- 'public can'
- );
-
- try {
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
- $exception = null;
- } catch ( SuppressedDataException $ex ) {
- $exception = $ex;
- }
-
- $this->assertSame(
- $userCan,
- $exception === null,
- 'user can'
- );
- }
-
- /**
- * @dataProvider provideGetSlot_audience
- */
- public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
- $this->forceStandardPermissions();
-
- $user = $this->getTestUser( $groups )->getUser();
- $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
-
- $this->assertNotNull( $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
-
- $this->assertSame(
- $publicCan,
- $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ) !== null,
- 'public can'
- );
- $this->assertSame(
- $userCan,
- $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ) !== null,
- 'user can'
- );
- }
-
- public function testGetSlot() {
- $rev = $this->newRevision();
-
- $slot = $rev->getSlot( SlotRecord::MAIN );
- $this->assertNotNull( $slot, 'getSlot()' );
- $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
- }
-
- public function testHasSlot() {
- $rev = $this->newRevision();
-
- $this->assertTrue( $rev->hasSlot( SlotRecord::MAIN ) );
- $this->assertFalse( $rev->hasSlot( 'xyz' ) );
- }
-
- public function testGetContent() {
- $rev = $this->newRevision();
-
- $content = $rev->getSlot( SlotRecord::MAIN );
- $this->assertNotNull( $content, 'getContent()' );
- $this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
- }
-
- public function provideUserCanBitfield() {
- yield [ 0, 0, [], null, true ];
- // Bitfields match, user has no permissions
- yield [
- RevisionRecord::DELETED_TEXT,
- RevisionRecord::DELETED_TEXT,
- [],
- null,
- false
- ];
- yield [
- RevisionRecord::DELETED_COMMENT,
- RevisionRecord::DELETED_COMMENT,
- [],
- null,
- false,
- ];
- yield [
- RevisionRecord::DELETED_USER,
- RevisionRecord::DELETED_USER,
- [],
- null,
- false
- ];
- yield [
- RevisionRecord::DELETED_RESTRICTED,
- RevisionRecord::DELETED_RESTRICTED,
- [],
- null,
- false,
- ];
- // Bitfields match, user (admin) does have permissions
- yield [
- RevisionRecord::DELETED_TEXT,
- RevisionRecord::DELETED_TEXT,
- [ 'sysop' ],
- null,
- true,
- ];
- yield [
- RevisionRecord::DELETED_COMMENT,
- RevisionRecord::DELETED_COMMENT,
- [ 'sysop' ],
- null,
- true,
- ];
- yield [
- RevisionRecord::DELETED_USER,
- RevisionRecord::DELETED_USER,
- [ 'sysop' ],
- null,
- true,
- ];
- // Bitfields match, user (admin) does not have permissions
- yield [
- RevisionRecord::DELETED_RESTRICTED,
- RevisionRecord::DELETED_RESTRICTED,
- [ 'sysop' ],
- null,
- false,
- ];
- // Bitfields match, user (oversight) does have permissions
- yield [
- RevisionRecord::DELETED_RESTRICTED,
- RevisionRecord::DELETED_RESTRICTED,
- [ 'oversight' ],
- null,
- true,
- ];
- // Check permissions using the title
- yield [
- RevisionRecord::DELETED_TEXT,
- RevisionRecord::DELETED_TEXT,
- [ 'sysop' ],
- __METHOD__,
- true,
- ];
- yield [
- RevisionRecord::DELETED_TEXT,
- RevisionRecord::DELETED_TEXT,
- [],
- __METHOD__,
- false,
- ];
- }
-
- /**
- * @dataProvider provideUserCanBitfield
- * @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield
- */
- public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
- if ( is_string( $title ) ) {
- // NOTE: Data providers cannot instantiate Title objects! See T202641.
- $title = Title::newFromText( $title );
- }
-
- $this->forceStandardPermissions();
-
- $user = $this->getTestUser( $userGroups )->getUser();
-
- $this->assertSame(
- $expected,
- RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
- );
- }
-
- public function provideHasSameContent() {
- // Create some slots with content
- $mainA = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'A' ) );
- $mainB = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'B' ) );
- $auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
- $auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
-
- $initialRecordSpec = [ [ $mainA ], 12 ];
-
- return [
- 'same record object' => [
- true,
- $initialRecordSpec,
- $initialRecordSpec,
- ],
- 'same record content, different object' => [
- true,
- [ [ $mainA ], 12 ],
- [ [ $mainA ], 13 ],
- ],
- 'same record content, aux slot, different object' => [
- true,
- [ [ $auxA ], 12 ],
- [ [ $auxB ], 13 ],
- ],
- 'different content' => [
- false,
- [ [ $mainA ], 12 ],
- [ [ $mainB ], 13 ],
- ],
- 'different content and number of slots' => [
- false,
- [ [ $mainA ], 12 ],
- [ [ $mainA, $mainB ], 13 ],
- ],
- ];
- }
-
- /**
- * @note Do not call directly from a data provider! Data providers cannot instantiate
- * Title objects! See T202641.
- *
- * @param SlotRecord[] $slots
- * @param int $revId
- * @return RevisionStoreRecord
- */
- private function makeHasSameContentTestRecord( array $slots, $revId ) {
- $title = Title::newFromText( 'provideHasSameContent' );
- $title->resetArticleID( 19 );
- $slots = new RevisionSlots( $slots );
-
- return new RevisionStoreRecord(
- $title,
- new UserIdentityValue( 11, __METHOD__, 0 ),
- CommentStoreComment::newUnsavedComment( __METHOD__ ),
- (object)[
- 'rev_id' => strval( $revId ),
- 'rev_page' => strval( $title->getArticleID() ),
- 'rev_timestamp' => '20200101000000',
- 'rev_deleted' => 0,
- 'rev_minor_edit' => 0,
- 'rev_parent_id' => '5',
- 'rev_len' => $slots->computeSize(),
- 'rev_sha1' => $slots->computeSha1(),
- 'page_latest' => '18',
- ],
- $slots
- );
- }
-
- /**
- * @dataProvider provideHasSameContent
- * @covers \MediaWiki\Storage\RevisionRecord::hasSameContent
- * @group Database
- */
- public function testHasSameContent(
- $expected,
- $recordSpec1,
- $recordSpec2
- ) {
- $record1 = $this->makeHasSameContentTestRecord( ...$recordSpec1 );
- $record2 = $this->makeHasSameContentTestRecord( ...$recordSpec2 );
-
- $this->assertSame(
- $expected,
- $record1->hasSameContent( $record2 )
- );
- }
-
- public function provideIsDeleted() {
- yield 'no deletion' => [
- 0,
- [
- RevisionRecord::DELETED_TEXT => false,
- RevisionRecord::DELETED_COMMENT => false,
- RevisionRecord::DELETED_USER => false,
- RevisionRecord::DELETED_RESTRICTED => false,
- ]
- ];
- yield 'text deleted' => [
- RevisionRecord::DELETED_TEXT,
- [
- RevisionRecord::DELETED_TEXT => true,
- RevisionRecord::DELETED_COMMENT => false,
- RevisionRecord::DELETED_USER => false,
- RevisionRecord::DELETED_RESTRICTED => false,
- ]
- ];
- yield 'text and comment deleted' => [
- RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
- [
- RevisionRecord::DELETED_TEXT => true,
- RevisionRecord::DELETED_COMMENT => true,
- RevisionRecord::DELETED_USER => false,
- RevisionRecord::DELETED_RESTRICTED => false,
- ]
- ];
- yield 'all 4 deleted' => [
- RevisionRecord::DELETED_TEXT +
- RevisionRecord::DELETED_COMMENT +
- RevisionRecord::DELETED_RESTRICTED +
- RevisionRecord::DELETED_USER,
- [
- RevisionRecord::DELETED_TEXT => true,
- RevisionRecord::DELETED_COMMENT => true,
- RevisionRecord::DELETED_USER => true,
- RevisionRecord::DELETED_RESTRICTED => true,
- ]
- ];
- }
-
- /**
- * @dataProvider provideIsDeleted
- * @covers \MediaWiki\Storage\RevisionRecord::isDeleted
- */
- public function testIsDeleted( $revDeleted, $assertionMap ) {
- $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
- foreach ( $assertionMap as $deletionLevel => $expected ) {
- $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
- }
- }
-
- public function testIsReadyForInsertion() {
- $rev = $this->newRevision();
- $this->assertTrue( $rev->isReadyForInsertion() );
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\SlotRecord;
-use MediaWikiTestCase;
-use TextContent;
-use WikitextContent;
-
-class RevisionSlotsTest extends MediaWikiTestCase {
-
- /**
- * @param SlotRecord[] $slots
- * @return RevisionSlots
- */
- protected function newRevisionSlots( $slots = [] ) {
- return new RevisionSlots( $slots );
- }
-
- public function provideConstructorFailue() {
- yield 'not an array or callable' => [
- 'foo'
- ];
- yield 'array of the wrong thing' => [
- [ 1, 2, 3 ]
- ];
- }
-
- /**
- * @dataProvider provideConstructorFailue
- * @param $slots
- *
- * @covers \MediaWiki\Storage\RevisionSlots::__construct
- * @covers \MediaWiki\Storage\RevisionSlots::setSlotsInternal
- */
- public function testConstructorFailue( $slots ) {
- $this->setExpectedException( InvalidArgumentException::class );
-
- new RevisionSlots( $slots );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::getSlot
- */
- public function testGetSlot() {
- $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
- $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
- $this->assertSame( $mainSlot, $slots->getSlot( SlotRecord::MAIN ) );
- $this->assertSame( $auxSlot, $slots->getSlot( 'aux' ) );
- $this->setExpectedException( RevisionAccessException::class );
- $slots->getSlot( 'nothere' );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::hasSlot
- */
- public function testHasSlot() {
- $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
- $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
- $this->assertTrue( $slots->hasSlot( SlotRecord::MAIN ) );
- $this->assertTrue( $slots->hasSlot( 'aux' ) );
- $this->assertFalse( $slots->hasSlot( 'AUX' ) );
- $this->assertFalse( $slots->hasSlot( 'xyz' ) );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::getContent
- */
- public function testGetContent() {
- $mainContent = new WikitextContent( 'A' );
- $auxContent = new WikitextContent( 'B' );
- $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, $mainContent );
- $auxSlot = SlotRecord::newUnsaved( 'aux', $auxContent );
- $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
- $this->assertSame( $mainContent, $slots->getContent( SlotRecord::MAIN ) );
- $this->assertSame( $auxContent, $slots->getContent( 'aux' ) );
- $this->setExpectedException( RevisionAccessException::class );
- $slots->getContent( 'nothere' );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::getSlotRoles
- */
- public function testGetSlotRoles_someSlots() {
- $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
- $slots = $this->newRevisionSlots( [ $mainSlot, $auxSlot ] );
-
- $this->assertSame( [ 'main', 'aux' ], $slots->getSlotRoles() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::getSlotRoles
- */
- public function testGetSlotRoles_noSlots() {
- $slots = $this->newRevisionSlots( [] );
-
- $this->assertSame( [], $slots->getSlotRoles() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::getSlots
- */
- public function testGetSlots() {
- $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $auxSlot = SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) );
- $slotsArray = [ $mainSlot, $auxSlot ];
- $slots = $this->newRevisionSlots( $slotsArray );
-
- $this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::getInheritedSlots
- */
- public function testGetInheritedSlots() {
- $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $auxSlot = SlotRecord::newInherited(
- SlotRecord::newSaved(
- 7, 7, 'foo',
- SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
- )
- );
- $slotsArray = [ $mainSlot, $auxSlot ];
- $slots = $this->newRevisionSlots( $slotsArray );
-
- $this->assertEquals( [ 'aux' => $auxSlot ], $slots->getInheritedSlots() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionSlots::getOriginalSlots
- */
- public function testGetOriginalSlots() {
- $mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $auxSlot = SlotRecord::newInherited(
- SlotRecord::newSaved(
- 7, 7, 'foo',
- SlotRecord::newUnsaved( 'aux', new WikitextContent( 'B' ) )
- )
- );
- $slotsArray = [ $mainSlot, $auxSlot ];
- $slots = $this->newRevisionSlots( $slotsArray );
-
- $this->assertEquals( [ 'main' => $mainSlot ], $slots->getOriginalSlots() );
- }
-
- public function provideComputeSize() {
- yield [ 1, [ 'A' ] ];
- yield [ 2, [ 'AA' ] ];
- yield [ 4, [ 'AA', 'X', 'H' ] ];
- }
-
- /**
- * @dataProvider provideComputeSize
- * @covers \MediaWiki\Storage\RevisionSlots::computeSize
- */
- public function testComputeSize( $expected, $contentStrings ) {
- $slotsArray = [];
- foreach ( $contentStrings as $key => $contentString ) {
- $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
- }
- $slots = $this->newRevisionSlots( $slotsArray );
-
- $this->assertSame( $expected, $slots->computeSize() );
- }
-
- public function provideComputeSha1() {
- yield [ 'ctqm7794fr2dp1taki8a88ovwnvmnmj', [ 'A' ] ];
- yield [ 'eyq8wiwlcofnaiy4eid97gyfy60uw51', [ 'AA' ] ];
- yield [ 'lavctqfpxartyjr31f853drgfl4kj1g', [ 'AA', 'X', 'H' ] ];
- }
-
- /**
- * @dataProvider provideComputeSha1
- * @covers \MediaWiki\Storage\RevisionSlots::computeSha1
- * @note this test is a bit brittle as the hashes are hardcoded, perhaps just check that strings
- * are returned and different Slots objects return different strings?
- */
- public function testComputeSha1( $expected, $contentStrings ) {
- $slotsArray = [];
- foreach ( $contentStrings as $key => $contentString ) {
- $slotsArray[] = SlotRecord::newUnsaved( strval( $key ), new WikitextContent( $contentString ) );
- }
- $slots = $this->newRevisionSlots( $slotsArray );
-
- $this->assertSame( $expected, $slots->computeSha1() );
- }
-
- public function provideHasSameContent() {
- $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
- $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
- $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
- $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
- $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
-
- $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
- $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
- $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
- $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
- $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
-
- yield 'same instance' => [ $a, $a, true ];
- yield 'same slots' => [ $a, $a2, true ];
- yield 'same content' => [ $a, $a3, true ];
-
- yield 'different roles' => [ $a, $b, false ];
- yield 'different content' => [ $a, $c, false ];
- }
-
- /**
- * @dataProvider provideHasSameContent
- * @covers \MediaWiki\Storage\RevisionSlots::hasSameContent
- */
- public function testHasSameContent( RevisionSlots $a, RevisionSlots $b, $same ) {
- $this->assertSame( $same, $a->hasSameContent( $b ) );
- $this->assertSame( $same, $b->hasSameContent( $a ) );
- }
-
- public function provideGetRolesWithDifferentContent() {
- $fooX = SlotRecord::newUnsaved( 'x', new TextContent( 'Foo' ) );
- $barZ = SlotRecord::newUnsaved( 'z', new TextContent( 'Bar' ) );
- $fooY = SlotRecord::newUnsaved( 'y', new TextContent( 'Foo' ) );
- $barZS = SlotRecord::newSaved( 7, 7, 'xyz', $barZ );
- $barZ2 = SlotRecord::newUnsaved( 'z', new TextContent( 'Baz' ) );
-
- $a = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
- $a2 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ ] );
- $a3 = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZS ] );
- $b = $this->newRevisionSlots( [ 'y' => $fooY, 'z' => $barZ ] );
- $c = $this->newRevisionSlots( [ 'x' => $fooX, 'z' => $barZ2 ] );
-
- yield 'same instance' => [ $a, $a, [] ];
- yield 'same slots' => [ $a, $a2, [] ];
- yield 'same content' => [ $a, $a3, [] ];
-
- yield 'different roles' => [ $a, $b, [ 'x', 'y' ] ];
- yield 'different content' => [ $a, $c, [ 'z' ] ];
- }
-
- /**
- * @dataProvider provideGetRolesWithDifferentContent
- * @covers \MediaWiki\Storage\RevisionSlots::getRolesWithDifferentContent
- */
- public function testGetRolesWithDifferentContent( RevisionSlots $a, RevisionSlots $b, $roles ) {
- $this->assertArrayEquals( $roles, $a->getRolesWithDifferentContent( $b ) );
- $this->assertArrayEquals( $roles, $b->getRolesWithDifferentContent( $a ) );
- }
-
-}
namespace MediaWiki\Tests\Storage;
use Content;
-use MediaWiki\Storage\MutableRevisionSlots;
-use MediaWiki\Storage\RevisionSlots;
+use MediaWiki\Revision\MutableRevisionSlots;
+use MediaWiki\Revision\RevisionSlots;
+use MediaWiki\Revision\RevisionAccessException;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\SlotRecord;
use MediaWikiTestCase;
use WikitextContent;
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use Content;
-use Exception;
-use HashBagOStuff;
-use InvalidArgumentException;
-use Language;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use PHPUnit_Framework_MockObject_MockObject;
-use Revision;
-use TestUserRegistry;
-use Title;
-use WANObjectCache;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\DatabaseSqlite;
-use Wikimedia\Rdbms\FakeResultWrapper;
-use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\TransactionProfiler;
-use WikiPage;
-use WikitextContent;
-
-/**
- * @group Database
- * @group RevisionStore
- */
-abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
-
- /**
- * @var Title
- */
- private $testPageTitle;
-
- /**
- * @var WikiPage
- */
- private $testPage;
-
- /**
- * @return int
- */
- abstract protected function getMcrMigrationStage();
-
- /**
- * @return bool
- */
- protected function getContentHandlerUseDB() {
- return true;
- }
-
- /**
- * @return string[]
- */
- abstract protected function getMcrTablesToReset();
-
- public function setUp() {
- parent::setUp();
- $this->tablesUsed[] = 'archive';
- $this->tablesUsed[] = 'page';
- $this->tablesUsed[] = 'revision';
- $this->tablesUsed[] = 'comment';
-
- $this->tablesUsed += $this->getMcrTablesToReset();
-
- $this->setMwGlobals( [
- 'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
- 'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
- 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
- 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
- ] );
-
- $this->overrideMwServices();
- }
-
- protected function addCoreDBData() {
- // Blank out. This would fail with a modified schema, and we don't need it.
- }
-
- /**
- * @return Title
- */
- protected function getTestPageTitle() {
- if ( $this->testPageTitle ) {
- return $this->testPageTitle;
- }
-
- $this->testPageTitle = Title::newFromText( 'UTPage-' . __CLASS__ );
- return $this->testPageTitle;
- }
- /**
- * @return WikiPage
- */
- protected function getTestPage() {
- if ( $this->testPage ) {
- return $this->testPage;
- }
-
- $title = $this->getTestPageTitle();
- $this->testPage = WikiPage::factory( $title );
-
- if ( !$this->testPage->exists() ) {
- // Make sure we don't write to the live db.
- $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
-
- $user = static::getTestSysop()->getUser();
-
- $this->testPage->doEditContent(
- new WikitextContent( 'UTContent-' . __CLASS__ ),
- 'UTPageSummary-' . __CLASS__,
- EDIT_NEW | EDIT_SUPPRESS_RC,
- false,
- $user
- );
- }
-
- return $this->testPage;
- }
-
- /**
- * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
- */
- private function getLoadBalancerMock( array $server ) {
- $lb = $this->getMockBuilder( LoadBalancer::class )
- ->setMethods( [ 'reallyOpenConnection' ] )
- ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] )
- ->getMock();
-
- $lb->method( 'reallyOpenConnection' )->willReturnCallback(
- function ( array $server, $dbNameOverride ) {
- return $this->getDatabaseMock( $server );
- }
- );
-
- return $lb;
- }
-
- /**
- * @return Database|PHPUnit_Framework_MockObject_MockObject
- */
- private function getDatabaseMock( array $params ) {
- $db = $this->getMockBuilder( DatabaseSqlite::class )
- ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] )
- ->setConstructorArgs( [ $params ] )
- ->getMock();
-
- $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) );
- $db->method( 'isOpen' )->willReturn( true );
-
- return $db;
- }
-
- public function provideDomainCheck() {
- yield [ false, 'test', '' ];
- yield [ 'test', 'test', '' ];
-
- yield [ false, 'test', 'foo_' ];
- yield [ 'test-foo_', 'test', 'foo_' ];
-
- yield [ false, 'dash-test', '' ];
- yield [ 'dash-test', 'dash-test', '' ];
-
- yield [ false, 'underscore_test', 'foo_' ];
- yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ];
- }
-
- /**
- * @dataProvider provideDomainCheck
- * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId
- */
- public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
- $this->setMwGlobals(
- [
- 'wgDBname' => $dbName,
- 'wgDBprefix' => $dbPrefix,
- ]
- );
-
- $loadBalancer = $this->getLoadBalancerMock(
- [
- 'host' => '*dummy*',
- 'dbDirectory' => '*dummy*',
- 'user' => 'test',
- 'password' => 'test',
- 'flags' => 0,
- 'variables' => [],
- 'schema' => '',
- 'cliMode' => true,
- 'agent' => '',
- 'load' => 100,
- 'profiler' => null,
- 'trxProfiler' => new TransactionProfiler(),
- 'connLogger' => new \Psr\Log\NullLogger(),
- 'queryLogger' => new \Psr\Log\NullLogger(),
- 'errorLogger' => function () {
- },
- 'deprecationLogger' => function () {
- },
- 'type' => 'test',
- 'dbname' => $dbName,
- 'tablePrefix' => $dbPrefix,
- ]
- );
- $db = $loadBalancer->getConnection( DB_REPLICA );
-
- /** @var SqlBlobStore $blobStore */
- $blobStore = $this->getMockBuilder( SqlBlobStore::class )
- ->disableOriginalConstructor()
- ->getMock();
-
- $store = new RevisionStore(
- $loadBalancer,
- $blobStore,
- new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
- MediaWikiServices::getInstance()->getCommentStore(),
- MediaWikiServices::getInstance()->getContentModelStore(),
- MediaWikiServices::getInstance()->getSlotRoleStore(),
- $this->getMcrMigrationStage(),
- MediaWikiServices::getInstance()->getActorMigration(),
- $wikiId
- );
-
- $count = $store->countRevisionsByPageId( $db, 0 );
-
- // Dummy check to make PhpUnit happy. We are really only interested in
- // countRevisionsByPageId not failing due to the DB domain check.
- $this->assertSame( 0, $count );
- }
-
- protected function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) {
- $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() );
- $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() );
- $this->assertEquals( $l1->getFragment(), $l2->getFragment() );
- $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() );
- }
-
- protected function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) {
- $this->assertEquals(
- $r1->getPageAsLinkTarget()->getNamespace(),
- $r2->getPageAsLinkTarget()->getNamespace()
- );
-
- $this->assertEquals(
- $r1->getPageAsLinkTarget()->getText(),
- $r2->getPageAsLinkTarget()->getText()
- );
-
- if ( $r1->getParentId() ) {
- $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
- }
-
- $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() );
- $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() );
- $this->assertEquals( $r1->getComment(), $r2->getComment() );
- $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() );
- $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() );
- $this->assertEquals( $r1->getSha1(), $r2->getSha1() );
- $this->assertEquals( $r1->getSize(), $r2->getSize() );
- $this->assertEquals( $r1->getPageId(), $r2->getPageId() );
- $this->assertArrayEquals( $r1->getSlotRoles(), $r2->getSlotRoles() );
- $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() );
- $this->assertEquals( $r1->isMinor(), $r2->isMinor() );
- foreach ( $r1->getSlotRoles() as $role ) {
- $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) );
- $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) );
- }
- foreach ( [
- RevisionRecord::DELETED_TEXT,
- RevisionRecord::DELETED_COMMENT,
- RevisionRecord::DELETED_USER,
- RevisionRecord::DELETED_RESTRICTED,
- ] as $field ) {
- $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) );
- }
- }
-
- protected function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) {
- $this->assertSame( $s1->getRole(), $s2->getRole() );
- $this->assertSame( $s1->getModel(), $s2->getModel() );
- $this->assertSame( $s1->getFormat(), $s2->getFormat() );
- $this->assertSame( $s1->getSha1(), $s2->getSha1() );
- $this->assertSame( $s1->getSize(), $s2->getSize() );
- $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) );
-
- $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null;
- $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null;
- }
-
- protected function assertRevisionCompleteness( RevisionRecord $r ) {
- $this->assertTrue( $r->hasSlot( SlotRecord::MAIN ) );
- $this->assertInstanceOf( SlotRecord::class, $r->getSlot( SlotRecord::MAIN ) );
- $this->assertInstanceOf( Content::class, $r->getContent( SlotRecord::MAIN ) );
-
- foreach ( $r->getSlotRoles() as $role ) {
- $this->assertSlotCompleteness( $r, $r->getSlot( $role ) );
- }
- }
-
- protected function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
- $this->assertTrue( $slot->hasAddress() );
- $this->assertSame( $r->getId(), $slot->getRevision() );
-
- $this->assertInstanceOf( Content::class, $slot->getContent() );
- }
-
- /**
- * @param mixed[] $details
- *
- * @return RevisionRecord
- */
- private function getRevisionRecordFromDetailsArray( $details = [] ) {
- // Convert some values that can't be provided by dataProviders
- if ( isset( $details['user'] ) && $details['user'] === true ) {
- $details['user'] = $this->getTestUser()->getUser();
- }
- if ( isset( $details['page'] ) && $details['page'] === true ) {
- $details['page'] = $this->getTestPage()->getId();
- }
- if ( isset( $details['parent'] ) && $details['parent'] === true ) {
- $details['parent'] = $this->getTestPage()->getLatest();
- }
-
- // Create the RevisionRecord with any available data
- $rev = new MutableRevisionRecord( $this->getTestPageTitle() );
- isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null;
- isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null;
- isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null;
- isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null;
- isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null;
- isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null;
- isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null;
- isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null;
- isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null;
- isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null;
- isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null;
-
- if ( isset( $details['content'] ) ) {
- foreach ( $details['content'] as $role => $content ) {
- $rev->setContent( $role, $content );
- }
- }
-
- return $rev;
- }
-
- public function provideInsertRevisionOn_successes() {
- yield 'Bare minimum revision insertion' => [
- [
- 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
- 'page' => true,
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- ];
- yield 'Detailed revision insertion' => [
- [
- 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
- 'parent' => true,
- 'page' => true,
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- 'minor' => true,
- 'visibility' => RevisionRecord::DELETED_RESTRICTED,
- ],
- ];
- }
-
- protected function getRandomCommentStoreComment() {
- return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
- }
-
- /**
- * @dataProvider provideInsertRevisionOn_successes
- * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
- * @covers \MediaWiki\Storage\RevisionStore::insertSlotRowOn
- * @covers \MediaWiki\Storage\RevisionStore::insertContentRowOn
- */
- public function testInsertRevisionOn_successes(
- array $revDetails = []
- ) {
- $title = $this->getTestPageTitle();
- $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
-
- $this->overrideMwServices();
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
-
- // is the new revision correct?
- $this->assertRevisionCompleteness( $return );
- $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() );
- $this->assertRevisionRecordsEqual( $rev, $return );
-
- // can we load it from the store?
- $loaded = $store->getRevisionById( $return->getId() );
- $this->assertRevisionCompleteness( $loaded );
- $this->assertRevisionRecordsEqual( $return, $loaded );
-
- // can we find it directly in the database?
- $this->assertRevisionExistsInDatabase( $return );
- }
-
- protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
- $row = $this->revisionToRow( new Revision( $rev ), [] );
-
- // unset nulled fields
- unset( $row->rev_content_model );
- unset( $row->rev_content_format );
-
- // unset fake fields
- unset( $row->rev_comment_text );
- unset( $row->rev_comment_data );
- unset( $row->rev_comment_cid );
- unset( $row->rev_comment_id );
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $queryInfo = $store->getQueryInfo( [ 'user' ] );
-
- $row = get_object_vars( $row );
- $this->assertSelect(
- $queryInfo['tables'],
- array_keys( $row ),
- [ 'rev_id' => $rev->getId() ],
- [ array_values( $row ) ],
- [],
- $queryInfo['joins']
- );
- }
-
- /**
- * @param SlotRecord $a
- * @param SlotRecord $b
- */
- protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
- // Assert that the same blob address has been used.
- $this->assertSame( $a->getAddress(), $b->getAddress() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
- */
- public function testInsertRevisionOn_blobAddressExists() {
- $title = $this->getTestPageTitle();
- $revDetails = [
- 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
- 'parent' => true,
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ];
-
- $this->overrideMwServices();
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- // Insert the first revision
- $revOne = $this->getRevisionRecordFromDetailsArray( $revDetails );
- $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) );
- $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() );
- $this->assertRevisionRecordsEqual( $revOne, $firstReturn );
-
- // Insert a second revision inheriting the same blob address
- $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( SlotRecord::MAIN ) );
- $revTwo = $this->getRevisionRecordFromDetailsArray( $revDetails );
- $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) );
- $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() );
- $this->assertRevisionRecordsEqual( $revTwo, $secondReturn );
-
- $firstMainSlot = $firstReturn->getSlot( SlotRecord::MAIN );
- $secondMainSlot = $secondReturn->getSlot( SlotRecord::MAIN );
-
- $this->assertSameSlotContent( $firstMainSlot, $secondMainSlot );
-
- // And that different revisions have been created.
- $this->assertNotSame( $firstReturn->getId(), $secondReturn->getId() );
-
- // Make sure the slot rows reference the correct revision
- $this->assertSame( $firstReturn->getId(), $firstMainSlot->getRevision() );
- $this->assertSame( $secondReturn->getId(), $secondMainSlot->getRevision() );
- }
-
- public function provideInsertRevisionOn_failures() {
- yield 'no slot' => [
- [
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- new InvalidArgumentException( 'main slot must be provided' )
- ];
- yield 'no main slot' => [
- [
- 'slot' => SlotRecord::newUnsaved( 'aux', new WikitextContent( 'Turkey' ) ),
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- new InvalidArgumentException( 'main slot must be provided' )
- ];
- yield 'no timestamp' => [
- [
- 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
- 'comment' => $this->getRandomCommentStoreComment(),
- 'user' => true,
- ],
- new IncompleteRevisionException( 'timestamp field must not be NULL!' )
- ];
- yield 'no comment' => [
- [
- 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
- 'timestamp' => '20171117010101',
- 'user' => true,
- ],
- new IncompleteRevisionException( 'comment must not be NULL!' )
- ];
- yield 'no user' => [
- [
- 'slot' => SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'Chicken' ) ),
- 'comment' => $this->getRandomCommentStoreComment(),
- 'timestamp' => '20171117010101',
- ],
- new IncompleteRevisionException( 'user must not be NULL!' )
- ];
- }
-
- /**
- * @dataProvider provideInsertRevisionOn_failures
- * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
- */
- public function testInsertRevisionOn_failures(
- array $revDetails = [],
- Exception $exception
- ) {
- $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- $this->setExpectedException(
- get_class( $exception ),
- $exception->getMessage(),
- $exception->getCode()
- );
- $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
- }
-
- public function provideNewNullRevision() {
- yield [
- Title::newFromText( 'UTPage_notAutoCreated' ),
- [ 'content' => [ 'main' => new WikitextContent( 'Flubber1' ) ] ],
- CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ),
- true,
- ];
- yield [
- Title::newFromText( 'UTPage_notAutoCreated' ),
- [ 'content' => [ 'main' => new WikitextContent( 'Flubber2' ) ] ],
- CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ),
- false,
- ];
- }
-
- /**
- * @dataProvider provideNewNullRevision
- * @covers \MediaWiki\Storage\RevisionStore::newNullRevision
- * @covers \MediaWiki\Storage\RevisionStore::findSlotContentId
- */
- public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
- $this->overrideMwServices();
-
- $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
- $page = WikiPage::factory( $title );
-
- if ( !$page->exists() ) {
- $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__, EDIT_NEW );
- }
-
- $revDetails['page'] = $page->getId();
- $revDetails['timestamp'] = wfTimestampNow();
- $revDetails['comment'] = CommentStoreComment::newUnsavedComment( 'Base' );
- $revDetails['user'] = $user;
-
- $baseRev = $this->getRevisionRecordFromDetailsArray( $revDetails );
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- $dbw = wfGetDB( DB_MASTER );
- $baseRev = $store->insertRevisionOn( $baseRev, $dbw );
- $page->updateRevisionOn( $dbw, new Revision( $baseRev ), $page->getLatest() );
-
- $record = $store->newNullRevision(
- wfGetDB( DB_MASTER ),
- $title,
- $comment,
- $minor,
- $user
- );
-
- $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() );
- $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() );
- $this->assertEquals( $comment, $record->getComment() );
- $this->assertEquals( $minor, $record->isMinor() );
- $this->assertEquals( $user->getName(), $record->getUser()->getName() );
- $this->assertEquals( $baseRev->getId(), $record->getParentId() );
-
- $this->assertArrayEquals(
- $baseRev->getSlotRoles(),
- $record->getSlotRoles()
- );
-
- foreach ( $baseRev->getSlotRoles() as $role ) {
- $parentSlot = $baseRev->getSlot( $role );
- $slot = $record->getSlot( $role );
-
- $this->assertTrue( $slot->isInherited(), 'isInherited' );
- $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
- $this->assertSameSlotContent( $parentSlot, $slot );
- }
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newNullRevision
- */
- public function testNewNullRevision_nonExistingTitle() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $record = $store->newNullRevision(
- wfGetDB( DB_MASTER ),
- Title::newFromText( __METHOD__ . '.iDontExist!' ),
- CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ),
- false,
- TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser()
- );
- $this->assertNull( $record );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
- */
- public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() {
- $page = $this->getTestPage();
- $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
- /** @var Revision $rev */
- $rev = $status->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revisionRecord = $store->getRevisionById( $rev->getId() );
- $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
-
- $this->assertGreaterThan( 0, $result );
- $this->assertSame(
- $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
- $result
- );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
- */
- public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() {
- // This assumes that sysops are auto patrolled
- $sysop = $this->getTestSysop()->getUser();
- $page = $this->getTestPage();
- $status = $page->doEditContent(
- new WikitextContent( __METHOD__ ),
- __METHOD__,
- 0,
- false,
- $sysop
- );
- /** @var Revision $rev */
- $rev = $status->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revisionRecord = $store->getRevisionById( $rev->getId() );
- $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
-
- $this->assertSame( 0, $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getRecentChange
- */
- public function testGetRecentChange() {
- $page = $this->getTestPage();
- $content = new WikitextContent( __METHOD__ );
- $status = $page->doEditContent( $content, __METHOD__ );
- /** @var Revision $rev */
- $rev = $status->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revRecord = $store->getRevisionById( $rev->getId() );
- $recentChange = $store->getRecentChange( $revRecord );
-
- $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
- $this->assertEquals( $rev->getRecentChange(), $recentChange );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getRevisionById
- */
- public function testGetRevisionById() {
- $page = $this->getTestPage();
- $content = new WikitextContent( __METHOD__ );
- $status = $page->doEditContent( $content, __METHOD__ );
- /** @var Revision $rev */
- $rev = $status->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revRecord = $store->getRevisionById( $rev->getId() );
-
- $this->assertSame( $rev->getId(), $revRecord->getId() );
- $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
- $this->assertSame( __METHOD__, $revRecord->getComment()->text );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle
- */
- public function testGetRevisionByTitle() {
- $page = $this->getTestPage();
- $content = new WikitextContent( __METHOD__ );
- $status = $page->doEditContent( $content, __METHOD__ );
- /** @var Revision $rev */
- $rev = $status->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revRecord = $store->getRevisionByTitle( $page->getTitle() );
-
- $this->assertSame( $rev->getId(), $revRecord->getId() );
- $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
- $this->assertSame( __METHOD__, $revRecord->getComment()->text );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId
- */
- public function testGetRevisionByPageId() {
- $page = $this->getTestPage();
- $content = new WikitextContent( __METHOD__ );
- $status = $page->doEditContent( $content, __METHOD__ );
- /** @var Revision $rev */
- $rev = $status->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revRecord = $store->getRevisionByPageId( $page->getId() );
-
- $this->assertSame( $rev->getId(), $revRecord->getId() );
- $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
- $this->assertSame( __METHOD__, $revRecord->getComment()->text );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp
- */
- public function testGetRevisionByTimestamp() {
- // Make sure there is 1 second between the last revision and the rev we create...
- // Otherwise we might not get the correct revision and the test may fail...
- // :(
- $page = $this->getTestPage();
- sleep( 1 );
- $content = new WikitextContent( __METHOD__ );
- $status = $page->doEditContent( $content, __METHOD__ );
- /** @var Revision $rev */
- $rev = $status->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revRecord = $store->getRevisionByTimestamp(
- $page->getTitle(),
- $rev->getTimestamp()
- );
-
- $this->assertSame( $rev->getId(), $revRecord->getId() );
- $this->assertTrue( $revRecord->getSlot( SlotRecord::MAIN )->getContent()->equals( $content ) );
- $this->assertSame( __METHOD__, $revRecord->getComment()->text );
- }
-
- protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
- // XXX: the WikiPage object loads another RevisionRecord from the database. Not great.
- $page = WikiPage::factory( $rev->getTitle() );
-
- $fields = [
- 'rev_id' => (string)$rev->getId(),
- 'rev_page' => (string)$rev->getPage(),
- 'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
- 'rev_user_text' => (string)$rev->getUserText(),
- 'rev_user' => (string)$rev->getUser(),
- 'rev_minor_edit' => $rev->isMinor() ? '1' : '0',
- 'rev_deleted' => (string)$rev->getVisibility(),
- 'rev_len' => (string)$rev->getSize(),
- 'rev_parent_id' => (string)$rev->getParentId(),
- 'rev_sha1' => (string)$rev->getSha1(),
- ];
-
- if ( in_array( 'page', $options ) ) {
- $fields += [
- 'page_namespace' => (string)$page->getTitle()->getNamespace(),
- 'page_title' => $page->getTitle()->getDBkey(),
- 'page_id' => (string)$page->getId(),
- 'page_latest' => (string)$page->getLatest(),
- 'page_is_redirect' => $page->isRedirect() ? '1' : '0',
- 'page_len' => (string)$page->getContent()->getSize(),
- ];
- }
-
- if ( in_array( 'user', $options ) ) {
- $fields += [
- 'user_name' => (string)$rev->getUserText(),
- ];
- }
-
- if ( in_array( 'comment', $options ) ) {
- $fields += [
- 'rev_comment_text' => $rev->getComment(),
- 'rev_comment_data' => null,
- 'rev_comment_cid' => null,
- ];
- }
-
- if ( $rev->getId() ) {
- $fields += [
- 'rev_id' => (string)$rev->getId(),
- ];
- }
-
- return (object)$fields;
- }
-
- protected function assertRevisionRecordMatchesRevision(
- Revision $rev,
- RevisionRecord $record
- ) {
- $this->assertSame( $rev->getId(), $record->getId() );
- $this->assertSame( $rev->getPage(), $record->getPageId() );
- $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() );
- $this->assertSame( $rev->getUserText(), $record->getUser()->getName() );
- $this->assertSame( $rev->getUser(), $record->getUser()->getId() );
- $this->assertSame( $rev->isMinor(), $record->isMinor() );
- $this->assertSame( $rev->getVisibility(), $record->getVisibility() );
- $this->assertSame( $rev->getSize(), $record->getSize() );
- /**
- * @note As of MW 1.31, the database schema allows the parent ID to be
- * NULL to indicate that it is unknown.
- */
- $expectedParent = $rev->getParentId();
- if ( $expectedParent === null ) {
- $expectedParent = 0;
- }
- $this->assertSame( $expectedParent, $record->getParentId() );
- $this->assertSame( $rev->getSha1(), $record->getSha1() );
- $this->assertSame( $rev->getComment(), $record->getComment()->text );
- $this->assertSame( $rev->getContentFormat(),
- $record->getContent( SlotRecord::MAIN )->getDefaultFormat() );
- $this->assertSame( $rev->getContentModel(), $record->getContent( SlotRecord::MAIN )->getModel() );
- $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() );
-
- $revRec = $rev->getRevisionRecord();
- $revMain = $revRec->getSlot( SlotRecord::MAIN );
- $recMain = $record->getSlot( SlotRecord::MAIN );
-
- $this->assertSame( $revMain->hasOrigin(), $recMain->hasOrigin(), 'hasOrigin' );
- $this->assertSame( $revMain->hasAddress(), $recMain->hasAddress(), 'hasAddress' );
- $this->assertSame( $revMain->hasContentId(), $recMain->hasContentId(), 'hasContentId' );
-
- if ( $revMain->hasOrigin() ) {
- $this->assertSame( $revMain->getOrigin(), $recMain->getOrigin(), 'getOrigin' );
- }
-
- if ( $revMain->hasAddress() ) {
- $this->assertSame( $revMain->getAddress(), $recMain->getAddress(), 'getAddress' );
- }
-
- if ( $revMain->hasContentId() ) {
- $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' );
- }
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
- */
- public function testNewRevisionFromRow_getQueryInfo() {
- $page = $this->getTestPage();
- $text = __METHOD__ . 'a-ä';
- /** @var Revision $rev */
- $rev = $page->doEditContent(
- new WikitextContent( $text ),
- __METHOD__ . 'a'
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $info = $store->getQueryInfo();
- $row = $this->db->selectRow(
- $info['tables'],
- $info['fields'],
- [ 'rev_id' => $rev->getId() ],
- __METHOD__,
- [],
- $info['joins']
- );
- $record = $store->newRevisionFromRow(
- $row,
- [],
- $page->getTitle()
- );
- $this->assertRevisionRecordMatchesRevision( $rev, $record );
- $this->assertSame( $text, $rev->getContent()->serialize() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- */
- public function testNewRevisionFromRow_anonEdit() {
- $page = $this->getTestPage();
- $text = __METHOD__ . 'a-ä';
- /** @var Revision $rev */
- $rev = $page->doEditContent(
- new WikitextContent( $text ),
- __METHOD__ . 'a'
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $record = $store->newRevisionFromRow(
- $this->revisionToRow( $rev ),
- [],
- $page->getTitle()
- );
- $this->assertRevisionRecordMatchesRevision( $rev, $record );
- $this->assertSame( $text, $rev->getContent()->serialize() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- */
- public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
- $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
- $this->overrideMwServices();
- $page = $this->getTestPage();
- $text = __METHOD__ . 'a-ä';
- /** @var Revision $rev */
- $rev = $page->doEditContent(
- new WikitextContent( $text ),
- __METHOD__ . 'a'
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $record = $store->newRevisionFromRow(
- $this->revisionToRow( $rev ),
- [],
- $page->getTitle()
- );
- $this->assertRevisionRecordMatchesRevision( $rev, $record );
- $this->assertSame( $text, $rev->getContent()->serialize() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- */
- public function testNewRevisionFromRow_userEdit() {
- $page = $this->getTestPage();
- $text = __METHOD__ . 'b-ä';
- /** @var Revision $rev */
- $rev = $page->doEditContent(
- new WikitextContent( $text ),
- __METHOD__ . 'b',
- 0,
- false,
- $this->getTestUser()->getUser()
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $record = $store->newRevisionFromRow(
- $this->revisionToRow( $rev ),
- [],
- $page->getTitle()
- );
- $this->assertRevisionRecordMatchesRevision( $rev, $record );
- $this->assertSame( $text, $rev->getContent()->serialize() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
- * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
- */
- public function testNewRevisionFromArchiveRow_getArchiveQueryInfo() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $title = Title::newFromText( __METHOD__ );
- $text = __METHOD__ . '-bä';
- $page = WikiPage::factory( $title );
- /** @var Revision $orig */
- $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
- ->value['revision'];
- $page->doDeleteArticle( __METHOD__ );
-
- $db = wfGetDB( DB_MASTER );
- $arQuery = $store->getArchiveQueryInfo();
- $res = $db->select(
- $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
- __METHOD__, [], $arQuery['joins']
- );
- $this->assertTrue( is_object( $res ), 'query failed' );
-
- $row = $res->fetchObject();
- $res->free();
- $record = $store->newRevisionFromArchiveRow( $row );
-
- $this->assertRevisionRecordMatchesRevision( $orig, $record );
- $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
- */
- public function testNewRevisionFromArchiveRow_legacyEncoding() {
- $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
- $this->overrideMwServices();
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $title = Title::newFromText( __METHOD__ );
- $text = __METHOD__ . '-bä';
- $page = WikiPage::factory( $title );
- /** @var Revision $orig */
- $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
- ->value['revision'];
- $page->doDeleteArticle( __METHOD__ );
-
- $db = wfGetDB( DB_MASTER );
- $arQuery = $store->getArchiveQueryInfo();
- $res = $db->select(
- $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
- __METHOD__, [], $arQuery['joins']
- );
- $this->assertTrue( is_object( $res ), 'query failed' );
-
- $row = $res->fetchObject();
- $res->free();
- $record = $store->newRevisionFromArchiveRow( $row );
-
- $this->assertRevisionRecordMatchesRevision( $orig, $record );
- $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
- */
- public function testNewRevisionFromArchiveRow_no_user() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- $row = (object)[
- 'ar_id' => '1',
- 'ar_page_id' => '2',
- 'ar_namespace' => '0',
- 'ar_title' => 'Something',
- 'ar_rev_id' => '2',
- 'ar_text_id' => '47',
- 'ar_timestamp' => '20180528192356',
- 'ar_minor_edit' => '0',
- 'ar_deleted' => '0',
- 'ar_len' => '78',
- 'ar_parent_id' => '0',
- 'ar_sha1' => 'deadbeef',
- 'ar_comment_text' => 'whatever',
- 'ar_comment_data' => null,
- 'ar_comment_cid' => null,
- 'ar_user' => '0',
- 'ar_user_text' => '', // this is the important bit
- 'ar_actor' => null,
- 'ar_content_format' => null,
- 'ar_content_model' => null,
- ];
-
- \Wikimedia\suppressWarnings();
- $record = $store->newRevisionFromArchiveRow( $row );
- \Wikimedia\suppressWarnings( true );
-
- $this->assertInstanceOf( RevisionRecord::class, $record );
- $this->assertInstanceOf( UserIdentityValue::class, $record->getUser() );
- $this->assertSame( 'Unknown user', $record->getUser()->getName() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- */
- public function testNewRevisionFromRow_no_user() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $title = Title::newFromText( __METHOD__ );
-
- $row = (object)[
- 'rev_id' => '2',
- 'rev_page' => '2',
- 'page_namespace' => '0',
- 'page_title' => $title->getText(),
- 'rev_text_id' => '47',
- 'rev_timestamp' => '20180528192356',
- 'rev_minor_edit' => '0',
- 'rev_deleted' => '0',
- 'rev_len' => '78',
- 'rev_parent_id' => '0',
- 'rev_sha1' => 'deadbeef',
- 'rev_comment_text' => 'whatever',
- 'rev_comment_data' => null,
- 'rev_comment_cid' => null,
- 'rev_user' => '0',
- 'rev_user_text' => '', // this is the important bit
- 'rev_actor' => null,
- 'rev_content_format' => null,
- 'rev_content_model' => null,
- ];
-
- \Wikimedia\suppressWarnings();
- $record = $store->newRevisionFromRow( $row, 0, $title );
- \Wikimedia\suppressWarnings( true );
-
- $this->assertNotNull( $record );
- $this->assertNotNull( $record->getUser() );
- $this->assertNotEmpty( $record->getUser()->getName() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
- */
- public function testInsertRevisionOn_archive() {
- // This is a round trip test for deletion and undeletion of a
- // revision row via the archive table.
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $title = Title::newFromText( __METHOD__ );
-
- $page = WikiPage::factory( $title );
- /** @var Revision $origRev */
- $page->doEditContent( new WikitextContent( "First" ), __METHOD__ . '-first' );
- $origRev = $page->doEditContent( new WikitextContent( "Foo" ), __METHOD__ )
- ->value['revision'];
- $orig = $origRev->getRevisionRecord();
- $page->doDeleteArticle( __METHOD__ );
-
- // re-create page, so we can later load revisions for it
- $page->doEditContent( new WikitextContent( 'Two' ), __METHOD__ );
-
- $db = wfGetDB( DB_MASTER );
- $arQuery = $store->getArchiveQueryInfo();
- $row = $db->selectRow(
- $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
- __METHOD__, [], $arQuery['joins']
- );
-
- $this->assertNotFalse( $row, 'query failed' );
-
- $record = $store->newRevisionFromArchiveRow(
- $row,
- 0,
- $title,
- [ 'page_id' => $title->getArticleID() ]
- );
-
- $restored = $store->insertRevisionOn( $record, $db );
-
- // is the new revision correct?
- $this->assertRevisionCompleteness( $restored );
- $this->assertRevisionRecordsEqual( $record, $restored );
-
- // does the new revision use the original slot?
- $recMain = $record->getSlot( SlotRecord::MAIN );
- $restMain = $restored->getSlot( SlotRecord::MAIN );
- $this->assertSame( $recMain->getAddress(), $restMain->getAddress() );
- $this->assertSame( $recMain->getContentId(), $restMain->getContentId() );
- $this->assertSame( $recMain->getOrigin(), $restMain->getOrigin() );
- $this->assertSame( 'Foo', $restMain->getContent()->serialize() );
-
- // can we load it from the store?
- $loaded = $store->getRevisionById( $restored->getId() );
- $this->assertNotNull( $loaded );
- $this->assertRevisionCompleteness( $loaded );
- $this->assertRevisionRecordsEqual( $restored, $loaded );
-
- // can we find it directly in the database?
- $this->assertRevisionExistsInDatabase( $restored );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId
- */
- public function testLoadRevisionFromId() {
- $title = Title::newFromText( __METHOD__ );
- $page = WikiPage::factory( $title );
- /** @var Revision $rev */
- $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
- ->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() );
- $this->assertRevisionRecordMatchesRevision( $rev, $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId
- */
- public function testLoadRevisionFromPageId() {
- $title = Title::newFromText( __METHOD__ );
- $page = WikiPage::factory( $title );
- /** @var Revision $rev */
- $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
- ->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() );
- $this->assertRevisionRecordMatchesRevision( $rev, $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle
- */
- public function testLoadRevisionFromTitle() {
- $title = Title::newFromText( __METHOD__ );
- $page = WikiPage::factory( $title );
- /** @var Revision $rev */
- $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
- ->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title );
- $this->assertRevisionRecordMatchesRevision( $rev, $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp
- */
- public function testLoadRevisionFromTimestamp() {
- $title = Title::newFromText( __METHOD__ );
- $page = WikiPage::factory( $title );
- /** @var Revision $revOne */
- $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
- ->value['revision'];
- // Sleep to ensure different timestamps... )(evil)
- sleep( 1 );
- /** @var Revision $revTwo */
- $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' )
- ->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $this->assertNull(
- $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' )
- );
- $this->assertSame(
- $revOne->getId(),
- $store->loadRevisionFromTimestamp(
- wfGetDB( DB_MASTER ),
- $title,
- $revOne->getTimestamp()
- )->getId()
- );
- $this->assertSame(
- $revTwo->getId(),
- $store->loadRevisionFromTimestamp(
- wfGetDB( DB_MASTER ),
- $title,
- $revTwo->getTimestamp()
- )->getId()
- );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes
- */
- public function testGetParentLengths() {
- $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
- /** @var Revision $revOne */
- $revOne = $page->doEditContent(
- new WikitextContent( __METHOD__ ), __METHOD__
- )->value['revision'];
- /** @var Revision $revTwo */
- $revTwo = $page->doEditContent(
- new WikitextContent( __METHOD__ . '2' ), __METHOD__
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $this->assertSame(
- [
- $revOne->getId() => strlen( __METHOD__ ),
- ],
- $store->listRevisionSizes(
- wfGetDB( DB_MASTER ),
- [ $revOne->getId() ]
- )
- );
- $this->assertSame(
- [
- $revOne->getId() => strlen( __METHOD__ ),
- $revTwo->getId() => strlen( __METHOD__ ) + 1,
- ],
- $store->listRevisionSizes(
- wfGetDB( DB_MASTER ),
- [ $revOne->getId(), $revTwo->getId() ]
- )
- );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision
- */
- public function testGetPreviousRevision() {
- $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
- /** @var Revision $revOne */
- $revOne = $page->doEditContent(
- new WikitextContent( __METHOD__ ), __METHOD__
- )->value['revision'];
- /** @var Revision $revTwo */
- $revTwo = $page->doEditContent(
- new WikitextContent( __METHOD__ . '2' ), __METHOD__
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $this->assertNull(
- $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) )
- );
- $this->assertSame(
- $revOne->getId(),
- $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId()
- );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getNextRevision
- */
- public function testGetNextRevision() {
- $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
- /** @var Revision $revOne */
- $revOne = $page->doEditContent(
- new WikitextContent( __METHOD__ ), __METHOD__
- )->value['revision'];
- /** @var Revision $revTwo */
- $revTwo = $page->doEditContent(
- new WikitextContent( __METHOD__ . '2' ), __METHOD__
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $this->assertSame(
- $revTwo->getId(),
- $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId()
- );
- $this->assertNull(
- $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) )
- );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
- */
- public function testGetTimestampFromId_found() {
- $page = $this->getTestPage();
- /** @var Revision $rev */
- $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
- ->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->getTimestampFromId(
- $page->getTitle(),
- $rev->getId()
- );
-
- $this->assertSame( $rev->getTimestamp(), $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
- */
- public function testGetTimestampFromId_notFound() {
- $page = $this->getTestPage();
- /** @var Revision $rev */
- $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
- ->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->getTimestampFromId(
- $page->getTitle(),
- $rev->getId() + 1
- );
-
- $this->assertFalse( $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId
- */
- public function testCountRevisionsByPageId() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
-
- $this->assertSame(
- 0,
- $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
- );
- $page->doEditContent( new WikitextContent( 'a' ), 'a' );
- $this->assertSame(
- 1,
- $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
- );
- $page->doEditContent( new WikitextContent( 'b' ), 'b' );
- $this->assertSame(
- 2,
- $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
- );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle
- */
- public function testCountRevisionsByTitle() {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
-
- $this->assertSame(
- 0,
- $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
- );
- $page->doEditContent( new WikitextContent( 'a' ), 'a' );
- $this->assertSame(
- 1,
- $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
- );
- $page->doEditContent( new WikitextContent( 'b' ), 'b' );
- $this->assertSame(
- 2,
- $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
- );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit
- */
- public function testUserWasLastToEdit_false() {
- $sysop = $this->getTestSysop()->getUser();
- $page = $this->getTestPage();
- $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->userWasLastToEdit(
- wfGetDB( DB_MASTER ),
- $page->getId(),
- $sysop->getId(),
- '20160101010101'
- );
- $this->assertFalse( $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit
- */
- public function testUserWasLastToEdit_true() {
- $startTime = wfTimestampNow();
- $sysop = $this->getTestSysop()->getUser();
- $page = $this->getTestPage();
- $page->doEditContent(
- new WikitextContent( __METHOD__ ),
- __METHOD__,
- 0,
- false,
- $sysop
- );
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $result = $store->userWasLastToEdit(
- wfGetDB( DB_MASTER ),
- $page->getId(),
- $sysop->getId(),
- $startTime
- );
- $this->assertTrue( $result );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision
- */
- public function testGetKnownCurrentRevision() {
- $page = $this->getTestPage();
- /** @var Revision $rev */
- $rev = $page->doEditContent(
- new WikitextContent( __METHOD__ . 'b' ),
- __METHOD__ . 'b',
- 0,
- false,
- $this->getTestUser()->getUser()
- )->value['revision'];
-
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $record = $store->getKnownCurrentRevision(
- $page->getTitle(),
- $rev->getId()
- );
-
- $this->assertRevisionRecordMatchesRevision( $rev, $record );
- }
-
- public function provideNewMutableRevisionFromArray() {
- yield 'Basic array, content object' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'content' => new WikitextContent( 'Some Content' ),
- ]
- ];
- yield 'Basic array, serialized text' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
- ]
- ];
- yield 'Basic array, serialized text, utf-8 flags' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
- 'flags' => 'utf-8',
- ]
- ];
- yield 'Basic array, with title' => [
- [
- 'title' => Title::newFromText( 'SomeText' ),
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.2',
- 'user' => 0,
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'content' => new WikitextContent( 'Some Content' ),
- ]
- ];
- yield 'Basic array, no user field' => [
- [
- 'id' => 2,
- 'page' => 1,
- 'timestamp' => '20171017114835',
- 'user_text' => '111.0.1.3',
- 'minor_edit' => false,
- 'deleted' => 0,
- 'len' => 46,
- 'parent_id' => 1,
- 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'comment' => 'Goat Comment!',
- 'content' => new WikitextContent( 'Some Content' ),
- ]
- ];
- }
-
- /**
- * @dataProvider provideNewMutableRevisionFromArray
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
- */
- public function testNewMutableRevisionFromArray( array $array ) {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
-
- // HACK: if $array['page'] is given, make sure that the page exists
- if ( isset( $array['page'] ) ) {
- $t = Title::newFromID( $array['page'] );
- if ( !$t || !$t->exists() ) {
- $t = Title::makeTitle( NS_MAIN, __METHOD__ );
- $info = $this->insertPage( $t );
- $array['page'] = $info['id'];
- }
- }
-
- $result = $store->newMutableRevisionFromArray( $array );
-
- if ( isset( $array['id'] ) ) {
- $this->assertSame( $array['id'], $result->getId() );
- }
- if ( isset( $array['page'] ) ) {
- $this->assertSame( $array['page'], $result->getPageId() );
- }
- $this->assertSame( $array['timestamp'], $result->getTimestamp() );
- $this->assertSame( $array['user_text'], $result->getUser()->getName() );
- if ( isset( $array['user'] ) ) {
- $this->assertSame( $array['user'], $result->getUser()->getId() );
- }
- $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() );
- $this->assertSame( $array['deleted'], $result->getVisibility() );
- $this->assertSame( $array['len'], $result->getSize() );
- $this->assertSame( $array['parent_id'], $result->getParentId() );
- $this->assertSame( $array['sha1'], $result->getSha1() );
- $this->assertSame( $array['comment'], $result->getComment()->text );
- if ( isset( $array['content'] ) ) {
- foreach ( $array['content'] as $role => $content ) {
- $this->assertTrue(
- $result->getContent( $role )->equals( $content )
- );
- }
- } elseif ( isset( $array['text'] ) ) {
- $this->assertSame( $array['text'],
- $result->getSlot( SlotRecord::MAIN )->getContent()->serialize() );
- } elseif ( isset( $array['content_format'] ) ) {
- $this->assertSame(
- $array['content_format'],
- $result->getSlot( SlotRecord::MAIN )->getContent()->getDefaultFormat()
- );
- $this->assertSame( $array['content_model'], $result->getSlot( SlotRecord::MAIN )->getModel() );
- }
- }
-
- /**
- * @dataProvider provideNewMutableRevisionFromArray
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
- */
- public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) {
- $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
- $blobStore = new SqlBlobStore( $lb, $cache );
- $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
-
- $factory = $this->getMockBuilder( BlobStoreFactory::class )
- ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] )
- ->disableOriginalConstructor()
- ->getMock();
- $factory->expects( $this->any() )
- ->method( 'newBlobStore' )
- ->willReturn( $blobStore );
- $factory->expects( $this->any() )
- ->method( 'newSqlBlobStore' )
- ->willReturn( $blobStore );
-
- $this->setService( 'BlobStoreFactory', $factory );
-
- $this->testNewMutableRevisionFromArray( $array );
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use ActorMigration;
-use CommentStore;
-use MediaWiki\Logger\Spi as LoggerSpi;
-use MediaWiki\Storage\BlobStore;
-use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\NameTableStore;
-use MediaWiki\Storage\NameTableStoreFactory;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreFactory;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use WANObjectCache;
-use Wikimedia\Rdbms\ILBFactory;
-use Wikimedia\Rdbms\ILoadBalancer;
-use Wikimedia\TestingAccessWrapper;
-
-class RevisionStoreFactoryTest extends MediaWikiTestCase {
-
- public function testValidConstruction_doesntCauseErrors() {
- new RevisionStoreFactory(
- $this->getMockLoadBalancerFactory(),
- $this->getMockBlobStoreFactory(),
- $this->getNameTableStoreFactory(),
- $this->getHashWANObjectCache(),
- $this->getMockCommentStore(),
- ActorMigration::newMigration(),
- MIGRATION_OLD,
- $this->getMockLoggerSpi(),
- true
- );
- $this->assertTrue( true );
- }
-
- public function provideWikiIds() {
- yield [ true ];
- yield [ false ];
- yield [ 'somewiki' ];
- yield [ 'somewiki', MIGRATION_OLD , false ];
- yield [ 'somewiki', MIGRATION_NEW , true ];
- }
-
- /**
- * @dataProvider provideWikiIds
- */
- public function testGetRevisionStore(
- $wikiId,
- $mcrMigrationStage = MIGRATION_OLD,
- $contentHandlerUseDb = true
- ) {
- $lbFactory = $this->getMockLoadBalancerFactory();
- $blobStoreFactory = $this->getMockBlobStoreFactory();
- $nameTableStoreFactory = $this->getNameTableStoreFactory();
- $cache = $this->getHashWANObjectCache();
- $commentStore = $this->getMockCommentStore();
- $actorMigration = ActorMigration::newMigration();
- $loggerProvider = $this->getMockLoggerSpi();
-
- $factory = new RevisionStoreFactory(
- $lbFactory,
- $blobStoreFactory,
- $nameTableStoreFactory,
- $cache,
- $commentStore,
- $actorMigration,
- $mcrMigrationStage,
- $loggerProvider,
- $contentHandlerUseDb
- );
-
- $store = $factory->getRevisionStore( $wikiId );
- $wrapper = TestingAccessWrapper::newFromObject( $store );
-
- // ensure the correct object type is returned
- $this->assertInstanceOf( RevisionStore::class, $store );
-
- // ensure the RevisionStore is for the given wikiId
- $this->assertSame( $wikiId, $wrapper->wikiId );
-
- // ensure all other required services are correctly set
- $this->assertSame( $cache, $wrapper->cache );
- $this->assertSame( $commentStore, $wrapper->commentStore );
- $this->assertSame( $mcrMigrationStage, $wrapper->mcrMigrationStage );
- $this->assertSame( $actorMigration, $wrapper->actorMigration );
- $this->assertSame( $contentHandlerUseDb, $store->getContentHandlerUseDB() );
-
- $this->assertInstanceOf( ILoadBalancer::class, $wrapper->loadBalancer );
- $this->assertInstanceOf( BlobStore::class, $wrapper->blobStore );
- $this->assertInstanceOf( NameTableStore::class, $wrapper->contentModelStore );
- $this->assertInstanceOf( NameTableStore::class, $wrapper->slotRoleStore );
- $this->assertInstanceOf( LoggerInterface::class, $wrapper->logger );
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
- */
- private function getMockLoadBalancer() {
- return $this->getMockBuilder( ILoadBalancer::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
- */
- private function getMockLoadBalancerFactory() {
- $mock = $this->getMockBuilder( ILBFactory::class )
- ->disableOriginalConstructor()->getMock();
-
- $mock->method( 'getMainLB' )
- ->willReturnCallback( function () {
- return $this->getMockLoadBalancer();
- } );
-
- return $mock;
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
- */
- private function getMockSqlBlobStore() {
- return $this->getMockBuilder( SqlBlobStore::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|BlobStoreFactory
- */
- private function getMockBlobStoreFactory() {
- $mock = $this->getMockBuilder( BlobStoreFactory::class )
- ->disableOriginalConstructor()->getMock();
-
- $mock->method( 'newSqlBlobStore' )
- ->willReturnCallback( function () {
- return $this->getMockSqlBlobStore();
- } );
-
- return $mock;
- }
-
- /**
- * @return NameTableStoreFactory
- */
- private function getNameTableStoreFactory() {
- return new NameTableStoreFactory(
- $this->getMockLoadBalancerFactory(),
- $this->getHashWANObjectCache(),
- new NullLogger() );
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
- */
- private function getMockCommentStore() {
- return $this->getMockBuilder( CommentStore::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- private function getHashWANObjectCache() {
- return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|LoggerSpi
- */
- private function getMockLoggerSpi() {
- $mock = $this->getMock( LoggerSpi::class );
-
- $mock->method( 'getLogger' )
- ->willReturn( new NullLogger() );
-
- return $mock;
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use InvalidArgumentException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionSlots;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiTestCase;
-use TextContent;
-use Title;
-
-/**
- * @covers \MediaWiki\Storage\RevisionStoreRecord
- * @covers \MediaWiki\Storage\RevisionRecord
- */
-class RevisionStoreRecordTest extends MediaWikiTestCase {
-
- use RevisionRecordTests;
-
- /**
- * @param array $rowOverrides
- *
- * @return RevisionStoreRecord
- */
- protected function newRevision( array $rowOverrides = [] ) {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- $user = new UserIdentityValue( 11, 'Tester', 0 );
- $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
- $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
- $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
- $slots = new RevisionSlots( [ $main, $aux ] );
-
- $row = [
- 'rev_id' => '7',
- 'rev_page' => strval( $title->getArticleID() ),
- 'rev_timestamp' => '20200101000000',
- 'rev_deleted' => 0,
- 'rev_minor_edit' => 0,
- 'rev_parent_id' => '5',
- 'rev_len' => $slots->computeSize(),
- 'rev_sha1' => $slots->computeSha1(),
- 'page_latest' => '18',
- ];
-
- $row = array_merge( $row, $rowOverrides );
-
- return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots );
- }
-
- public function provideConstructor() {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- $user = new UserIdentityValue( 11, 'Tester', 0 );
- $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
- $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
- $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
- $slots = new RevisionSlots( [ $main, $aux ] );
-
- $protoRow = [
- 'rev_id' => '7',
- 'rev_page' => strval( $title->getArticleID() ),
- 'rev_timestamp' => '20200101000000',
- 'rev_deleted' => 0,
- 'rev_minor_edit' => 0,
- 'rev_parent_id' => '5',
- 'rev_len' => $slots->computeSize(),
- 'rev_sha1' => $slots->computeSha1(),
- 'page_latest' => '18',
- ];
-
- $row = $protoRow;
- yield 'all info' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots,
- 'acmewiki'
- ];
-
- $row = $protoRow;
- $row['rev_minor_edit'] = '1';
- $row['rev_deleted'] = strval( RevisionRecord::DELETED_USER );
-
- yield 'minor deleted' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- $row['page_latest'] = $row['rev_id'];
-
- yield 'latest' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- unset( $row['rev_parent'] );
-
- yield 'no parent' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- $row['rev_len'] = null;
- $row['rev_sha1'] = '';
-
- yield 'rev_len is null, rev_sha1 is ""' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- yield 'no length, no hash' => [
- Title::newFromText( 'DummyDoesNotExist' ),
- $user,
- $comment,
- (object)$row,
- $slots
- ];
- }
-
- /**
- * @dataProvider provideConstructor
- *
- * @param Title $title
- * @param UserIdentity $user
- * @param CommentStoreComment $comment
- * @param object $row
- * @param RevisionSlots $slots
- * @param bool $wikiId
- */
- public function testConstructorAndGetters(
- Title $title,
- UserIdentity $user,
- CommentStoreComment $comment,
- $row,
- RevisionSlots $slots,
- $wikiId = false
- ) {
- $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
-
- $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
- $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
- $this->assertSame( $comment, $rec->getComment(), 'getComment' );
-
- $this->assertSame( $slots, $rec->getSlots(), 'getSlots' );
- $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
- $this->assertSame( $slots->getSlots(), $rec->getSlots()->getSlots(), 'getSlots' );
- $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
-
- $this->assertSame( (int)$row->rev_id, $rec->getId(), 'getId' );
- $this->assertSame( (int)$row->rev_page, $rec->getPageId(), 'getId' );
- $this->assertSame( $row->rev_timestamp, $rec->getTimestamp(), 'getTimestamp' );
- $this->assertSame( (int)$row->rev_deleted, $rec->getVisibility(), 'getVisibility' );
- $this->assertSame( (bool)$row->rev_minor_edit, $rec->isMinor(), 'getIsMinor' );
-
- if ( isset( $row->rev_parent_id ) ) {
- $this->assertSame( (int)$row->rev_parent_id, $rec->getParentId(), 'getParentId' );
- } else {
- $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
- }
-
- if ( isset( $row->rev_len ) ) {
- $this->assertSame( (int)$row->rev_len, $rec->getSize(), 'getSize' );
- } else {
- $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
- }
-
- if ( !empty( $row->rev_sha1 ) ) {
- $this->assertSame( $row->rev_sha1, $rec->getSha1(), 'getSha1' );
- } else {
- $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
- }
-
- if ( isset( $row->page_latest ) ) {
- $this->assertSame(
- (int)$row->rev_id === (int)$row->page_latest,
- $rec->isCurrent(),
- 'isCurrent'
- );
- } else {
- $this->assertSame(
- false,
- $rec->isCurrent(),
- 'isCurrent'
- );
- }
- }
-
- public function provideConstructorFailure() {
- $title = Title::newFromText( 'Dummy' );
- $title->resetArticleID( 17 );
-
- $user = new UserIdentityValue( 11, 'Tester', 0 );
-
- $comment = CommentStoreComment::newUnsavedComment( 'Hello World' );
-
- $main = SlotRecord::newUnsaved( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
- $aux = SlotRecord::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
- $slots = new RevisionSlots( [ $main, $aux ] );
-
- $protoRow = [
- 'rev_id' => '7',
- 'rev_page' => strval( $title->getArticleID() ),
- 'rev_timestamp' => '20200101000000',
- 'rev_deleted' => 0,
- 'rev_minor_edit' => 0,
- 'rev_parent_id' => '5',
- 'rev_len' => $slots->computeSize(),
- 'rev_sha1' => $slots->computeSha1(),
- 'page_latest' => '18',
- ];
-
- yield 'not a row' => [
- $title,
- $user,
- $comment,
- 'not a row',
- $slots,
- 'acmewiki'
- ];
-
- $row = $protoRow;
- $row['rev_timestamp'] = 'kittens';
-
- yield 'bad timestamp' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
- $row['rev_page'] = 99;
-
- yield 'page ID mismatch' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots
- ];
-
- $row = $protoRow;
-
- yield 'bad wiki' => [
- $title,
- $user,
- $comment,
- (object)$row,
- $slots,
- 12345
- ];
- }
-
- /**
- * @dataProvider provideConstructorFailure
- *
- * @param Title $title
- * @param UserIdentity $user
- * @param CommentStoreComment $comment
- * @param object $row
- * @param RevisionSlots $slots
- * @param bool $wikiId
- */
- public function testConstructorFailure(
- Title $title,
- UserIdentity $user,
- CommentStoreComment $comment,
- $row,
- RevisionSlots $slots,
- $wikiId = false
- ) {
- $this->setExpectedException( InvalidArgumentException::class );
- new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
- }
-
- public function provideIsCurrent() {
- yield [
- [
- 'rev_id' => 11,
- 'page_latest' => 11,
- ],
- true,
- ];
- yield [
- [
- 'rev_id' => 11,
- 'page_latest' => 10,
- ],
- false,
- ];
- }
-
- /**
- * @dataProvider provideIsCurrent
- */
- public function testIsCurrent( $row, $current ) {
- $rev = $this->newRevision( $row );
-
- $this->assertSame( $current, $rev->isCurrent(), 'isCurrent()' );
- }
-
- public function provideGetSlot_audience_latest() {
- return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
- }
-
- /**
- * @dataProvider provideGetSlot_audience_latest
- */
- public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) {
- $this->forceStandardPermissions();
-
- $user = $this->getTestUser( $groups )->getUser();
- $rev = $this->newRevision(
- [
- 'rev_deleted' => $visibility,
- 'rev_id' => 11,
- 'page_latest' => 11, // revision is current
- ]
- );
-
- // NOTE: slot meta-data is never suppressed, just the content is!
- $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW ), 'raw can' );
- $this->assertNotNull( $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC ),
- 'public can' );
-
- $this->assertNotNull(
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user ),
- 'user can'
- );
-
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getContent();
- // NOTE: the content of the current revision is never suppressed!
- // Check that getContent() doesn't throw SuppressedDataException
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC )->getContent();
- $rev->getSlot( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )->getContent();
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStore;
-use HashBagOStuff;
-use InvalidArgumentException;
-use Language;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use MWException;
-use Title;
-use WANObjectCache;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\TestingAccessWrapper;
-use WikitextContent;
-
-class RevisionStoreTest extends MediaWikiTestCase {
-
- private function useTextId() {
- global $wgMultiContentRevisionSchemaMigrationStage;
-
- return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
- }
-
- /**
- * @param LoadBalancer $loadBalancer
- * @param SqlBlobStore $blobStore
- * @param WANObjectCache $WANObjectCache
- *
- * @return RevisionStore
- */
- private function getRevisionStore(
- $loadBalancer = null,
- $blobStore = null,
- $WANObjectCache = null
- ) {
- global $wgMultiContentRevisionSchemaMigrationStage;
- // the migration stage should be irrelevant, since all the tests that interact with
- // the database are in RevisionStoreDbTest, not here.
-
- return new RevisionStore(
- $loadBalancer ?: $this->getMockLoadBalancer(),
- $blobStore ?: $this->getMockSqlBlobStore(),
- $WANObjectCache ?: $this->getHashWANObjectCache(),
- MediaWikiServices::getInstance()->getCommentStore(),
- MediaWikiServices::getInstance()->getContentModelStore(),
- MediaWikiServices::getInstance()->getSlotRoleStore(),
- $wgMultiContentRevisionSchemaMigrationStage,
- MediaWikiServices::getInstance()->getActorMigration()
- );
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
- */
- private function getMockLoadBalancer() {
- return $this->getMockBuilder( LoadBalancer::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|Database
- */
- private function getMockDatabase() {
- return $this->getMockBuilder( Database::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
- */
- private function getMockSqlBlobStore() {
- return $this->getMockBuilder( SqlBlobStore::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
- */
- private function getMockCommentStore() {
- return $this->getMockBuilder( CommentStore::class )
- ->disableOriginalConstructor()->getMock();
- }
-
- private function getHashWANObjectCache() {
- return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
- }
-
- public function provideSetContentHandlerUseDB() {
- return [
- // ContentHandlerUseDB can be true of false pre migration.
- [ false, SCHEMA_COMPAT_OLD, false ],
- [ true, SCHEMA_COMPAT_OLD, false ],
- // During and after migration it can not be false...
- [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, true ],
- [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, true ],
- [ false, SCHEMA_COMPAT_NEW, true ],
- // ...but it can be true.
- [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
- [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
- [ true, SCHEMA_COMPAT_NEW, false ],
- ];
- }
-
- /**
- * @dataProvider provideSetContentHandlerUseDB
- * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
- * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
- */
- public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
- if ( $expectedFail ) {
- $this->setExpectedException( MWException::class );
- }
-
- $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
-
- $store = new RevisionStore(
- $this->getMockLoadBalancer(),
- $this->getMockSqlBlobStore(),
- $this->getHashWANObjectCache(),
- $this->getMockCommentStore(),
- $nameTables->getContentModels(),
- $nameTables->getSlotRoles(),
- $migrationMode,
- MediaWikiServices::getInstance()->getActorMigration()
- );
-
- $store->setContentHandlerUseDB( $contentHandlerDb );
- $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
- }
-
- public function testGetTitle_successFromPageId() {
- $mockLoadBalancer = $this->getMockLoadBalancer();
- // Title calls wfGetDB() so we have to set the main service
- $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
- $db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
- $mockLoadBalancer->expects( $this->atLeastOnce() )
- ->method( 'getConnection' )
- ->willReturn( $db );
-
- // First call to Title::newFromID, faking no result (db lag?)
- $db->expects( $this->at( 0 ) )
- ->method( 'selectRow' )
- ->with(
- 'page',
- $this->anything(),
- [ 'page_id' => 1 ]
- )
- ->willReturn( (object)[
- 'page_namespace' => '1',
- 'page_title' => 'Food',
- ] );
-
- $store = $this->getRevisionStore( $mockLoadBalancer );
- $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
- $this->assertSame( 1, $title->getNamespace() );
- $this->assertSame( 'Food', $title->getDBkey() );
- }
-
- public function testGetTitle_successFromPageIdOnFallback() {
- $mockLoadBalancer = $this->getMockLoadBalancer();
- // Title calls wfGetDB() so we have to set the main service
- $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
- $db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
- // Assert that the first call uses a REPLICA and the second falls back to master
- $mockLoadBalancer->expects( $this->exactly( 2 ) )
- ->method( 'getConnection' )
- ->willReturn( $db );
- // RevisionStore getTitle uses a ConnectionRef
- $mockLoadBalancer->expects( $this->atLeastOnce() )
- ->method( 'getConnectionRef' )
- ->willReturn( $db );
-
- // First call to Title::newFromID, faking no result (db lag?)
- $db->expects( $this->at( 0 ) )
- ->method( 'selectRow' )
- ->with(
- 'page',
- $this->anything(),
- [ 'page_id' => 1 ]
- )
- ->willReturn( false );
-
- // First select using rev_id, faking no result (db lag?)
- $db->expects( $this->at( 1 ) )
- ->method( 'selectRow' )
- ->with(
- [ 'revision', 'page' ],
- $this->anything(),
- [ 'rev_id' => 2 ]
- )
- ->willReturn( false );
-
- // Second call to Title::newFromID, no result
- $db->expects( $this->at( 2 ) )
- ->method( 'selectRow' )
- ->with(
- 'page',
- $this->anything(),
- [ 'page_id' => 1 ]
- )
- ->willReturn( (object)[
- 'page_namespace' => '2',
- 'page_title' => 'Foodey',
- ] );
-
- $store = $this->getRevisionStore( $mockLoadBalancer );
- $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
- $this->assertSame( 2, $title->getNamespace() );
- $this->assertSame( 'Foodey', $title->getDBkey() );
- }
-
- public function testGetTitle_successFromRevId() {
- $mockLoadBalancer = $this->getMockLoadBalancer();
- // Title calls wfGetDB() so we have to set the main service
- $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
- $db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
- $mockLoadBalancer->expects( $this->atLeastOnce() )
- ->method( 'getConnection' )
- ->willReturn( $db );
- // RevisionStore getTitle uses a ConnectionRef
- $mockLoadBalancer->expects( $this->atLeastOnce() )
- ->method( 'getConnectionRef' )
- ->willReturn( $db );
-
- // First call to Title::newFromID, faking no result (db lag?)
- $db->expects( $this->at( 0 ) )
- ->method( 'selectRow' )
- ->with(
- 'page',
- $this->anything(),
- [ 'page_id' => 1 ]
- )
- ->willReturn( false );
-
- // First select using rev_id, faking no result (db lag?)
- $db->expects( $this->at( 1 ) )
- ->method( 'selectRow' )
- ->with(
- [ 'revision', 'page' ],
- $this->anything(),
- [ 'rev_id' => 2 ]
- )
- ->willReturn( (object)[
- 'page_namespace' => '1',
- 'page_title' => 'Food2',
- ] );
-
- $store = $this->getRevisionStore( $mockLoadBalancer );
- $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
- $this->assertSame( 1, $title->getNamespace() );
- $this->assertSame( 'Food2', $title->getDBkey() );
- }
-
- public function testGetTitle_successFromRevIdOnFallback() {
- $mockLoadBalancer = $this->getMockLoadBalancer();
- // Title calls wfGetDB() so we have to set the main service
- $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
- $db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
- // Assert that the first call uses a REPLICA and the second falls back to master
- $mockLoadBalancer->expects( $this->exactly( 2 ) )
- ->method( 'getConnection' )
- ->willReturn( $db );
- // RevisionStore getTitle uses a ConnectionRef
- $mockLoadBalancer->expects( $this->atLeastOnce() )
- ->method( 'getConnectionRef' )
- ->willReturn( $db );
-
- // First call to Title::newFromID, faking no result (db lag?)
- $db->expects( $this->at( 0 ) )
- ->method( 'selectRow' )
- ->with(
- 'page',
- $this->anything(),
- [ 'page_id' => 1 ]
- )
- ->willReturn( false );
-
- // First select using rev_id, faking no result (db lag?)
- $db->expects( $this->at( 1 ) )
- ->method( 'selectRow' )
- ->with(
- [ 'revision', 'page' ],
- $this->anything(),
- [ 'rev_id' => 2 ]
- )
- ->willReturn( false );
-
- // Second call to Title::newFromID, no result
- $db->expects( $this->at( 2 ) )
- ->method( 'selectRow' )
- ->with(
- 'page',
- $this->anything(),
- [ 'page_id' => 1 ]
- )
- ->willReturn( false );
-
- // Second select using rev_id, result
- $db->expects( $this->at( 3 ) )
- ->method( 'selectRow' )
- ->with(
- [ 'revision', 'page' ],
- $this->anything(),
- [ 'rev_id' => 2 ]
- )
- ->willReturn( (object)[
- 'page_namespace' => '2',
- 'page_title' => 'Foodey',
- ] );
-
- $store = $this->getRevisionStore( $mockLoadBalancer );
- $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
-
- $this->assertSame( 2, $title->getNamespace() );
- $this->assertSame( 'Foodey', $title->getDBkey() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::getTitle
- */
- public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
- $mockLoadBalancer = $this->getMockLoadBalancer();
- // Title calls wfGetDB() so we have to set the main service
- $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
-
- $db = $this->getMockDatabase();
- // Title calls wfGetDB() which uses a regular Connection
- // Assert that the first call uses a REPLICA and the second falls back to master
-
- // RevisionStore getTitle uses getConnectionRef
- // Title::newFromID uses getConnection
- foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
- $mockLoadBalancer->expects( $this->exactly( 2 ) )
- ->method( $method )
- ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
- static $callCounter = 0;
- $callCounter++;
- // The first call should be to a REPLICA, and the second a MASTER.
- if ( $callCounter === 1 ) {
- $this->assertSame( DB_REPLICA, $masterOrReplica );
- } elseif ( $callCounter === 2 ) {
- $this->assertSame( DB_MASTER, $masterOrReplica );
- }
- return $db;
- } );
- }
- // First and third call to Title::newFromID, faking no result
- foreach ( [ 0, 2 ] as $counter ) {
- $db->expects( $this->at( $counter ) )
- ->method( 'selectRow' )
- ->with(
- 'page',
- $this->anything(),
- [ 'page_id' => 1 ]
- )
- ->willReturn( false );
- }
-
- foreach ( [ 1, 3 ] as $counter ) {
- $db->expects( $this->at( $counter ) )
- ->method( 'selectRow' )
- ->with(
- [ 'revision', 'page' ],
- $this->anything(),
- [ 'rev_id' => 2 ]
- )
- ->willReturn( false );
- }
-
- $store = $this->getRevisionStore( $mockLoadBalancer );
-
- $this->setExpectedException( RevisionAccessException::class );
- $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
- }
-
- public function provideNewRevisionFromRow_legacyEncoding_applied() {
- yield 'windows-1252, old_flags is empty' => [
- 'windows-1252',
- 'en',
- [
- 'old_flags' => '',
- 'old_text' => "S\xF6me Content",
- ],
- 'Söme Content'
- ];
-
- yield 'windows-1252, old_flags is null' => [
- 'windows-1252',
- 'en',
- [
- 'old_flags' => null,
- 'old_text' => "S\xF6me Content",
- ],
- 'Söme Content'
- ];
- }
-
- /**
- * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
- *
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- */
- public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
- if ( !$this->useTextId() ) {
- $this->markTestSkipped( 'No longer applicable with MCR schema' );
- }
-
- $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-
- $blobStore = new SqlBlobStore( $lb, $cache );
- $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
-
- $store = $this->getRevisionStore( $lb, $blobStore, $cache );
-
- $record = $store->newRevisionFromRow(
- $this->makeRow( $row ),
- 0,
- Title::newFromText( __METHOD__ . '-UTPage' )
- );
-
- $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
- */
- public function testNewRevisionFromRow_legacyEncoding_ignored() {
- if ( !$this->useTextId() ) {
- $this->markTestSkipped( 'No longer applicable with MCR schema' );
- }
-
- $row = [
- 'old_flags' => 'utf-8',
- 'old_text' => 'Söme Content',
- ];
-
- $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
- $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-
- $blobStore = new SqlBlobStore( $lb, $cache );
- $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
-
- $store = $this->getRevisionStore( $lb, $blobStore, $cache );
-
- $record = $store->newRevisionFromRow(
- $this->makeRow( $row ),
- 0,
- Title::newFromText( __METHOD__ . '-UTPage' )
- );
- $this->assertSame( 'Söme Content', $record->getContent( SlotRecord::MAIN )->serialize() );
- }
-
- private function makeRow( array $array ) {
- $row = $array + [
- 'rev_id' => 7,
- 'rev_page' => 5,
- 'rev_timestamp' => '20110101000000',
- 'rev_user_text' => 'Tester',
- 'rev_user' => 17,
- 'rev_minor_edit' => 0,
- 'rev_deleted' => 0,
- 'rev_len' => 100,
- 'rev_parent_id' => 0,
- 'rev_sha1' => 'deadbeef',
- 'rev_comment_text' => 'Testing',
- 'rev_comment_data' => '{}',
- 'rev_comment_cid' => 111,
- 'page_namespace' => 0,
- 'page_title' => 'TEST',
- 'page_id' => 5,
- 'page_latest' => 7,
- 'page_is_redirect' => 0,
- 'page_len' => 100,
- 'user_name' => 'Tester',
- ];
-
- if ( $this->useTextId() ) {
- $row += [
- 'rev_content_format' => CONTENT_FORMAT_TEXT,
- 'rev_content_model' => CONTENT_MODEL_TEXT,
- 'rev_text_id' => 11,
- 'old_id' => 11,
- 'old_text' => 'Hello World',
- 'old_flags' => 'utf-8',
- ];
- } else {
- if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
- $row['content'] = [
- 'main' => new WikitextContent( $array['old_text'] ),
- ];
- }
- }
-
- return (object)$row;
- }
-
- public function provideMigrationConstruction() {
- return [
- [ SCHEMA_COMPAT_OLD, false ],
- [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
- [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
- [ SCHEMA_COMPAT_NEW, false ],
- [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, true ],
- [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, true ],
- [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, true ],
- ];
- }
-
- /**
- * @covers \MediaWiki\Storage\RevisionStore::__construct
- * @dataProvider provideMigrationConstruction
- */
- public function testMigrationConstruction( $migration, $expectException ) {
- if ( $expectException ) {
- $this->setExpectedException( InvalidArgumentException::class );
- }
- $loadBalancer = $this->getMockLoadBalancer();
- $blobStore = $this->getMockSqlBlobStore();
- $cache = $this->getHashWANObjectCache();
- $commentStore = $this->getMockCommentStore();
- $services = MediaWikiServices::getInstance();
- $nameTables = $services->getNameTableStoreFactory();
- $contentModelStore = $nameTables->getContentModels();
- $slotRoleStore = $nameTables->getSlotRoles();
- $store = new RevisionStore(
- $loadBalancer,
- $blobStore,
- $cache,
- $commentStore,
- $nameTables->getContentModels(),
- $nameTables->getSlotRoles(),
- $migration,
- $services->getActorMigration()
- );
- if ( !$expectException ) {
- $store = TestingAccessWrapper::newFromObject( $store );
- $this->assertSame( $loadBalancer, $store->loadBalancer );
- $this->assertSame( $blobStore, $store->blobStore );
- $this->assertSame( $cache, $store->cache );
- $this->assertSame( $commentStore, $store->commentStore );
- $this->assertSame( $contentModelStore, $store->contentModelStore );
- $this->assertSame( $slotRoleStore, $store->slotRoleStore );
- $this->assertSame( $migration, $store->mcrMigrationStage );
- }
- }
-
-}
+++ /dev/null
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use InvalidArgumentException;
-use LogicException;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SuppressedDataException;
-use MediaWikiTestCase;
-use WikitextContent;
-
-/**
- * @covers \MediaWiki\Storage\SlotRecord
- */
-class SlotRecordTest extends MediaWikiTestCase {
-
- private function makeRow( $data = [] ) {
- $data = $data + [
- 'slot_id' => 1234,
- 'slot_content_id' => 33,
- 'content_size' => '5',
- 'content_sha1' => 'someHash',
- 'content_address' => 'tt:456',
- 'model_name' => CONTENT_MODEL_WIKITEXT,
- 'format_name' => CONTENT_FORMAT_WIKITEXT,
- 'slot_revision_id' => '2',
- 'slot_origin' => '1',
- 'role_name' => 'myRole',
- ];
- return (object)$data;
- }
-
- public function testCompleteConstruction() {
- $row = $this->makeRow();
- $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
- $this->assertTrue( $record->hasAddress() );
- $this->assertTrue( $record->hasContentId() );
- $this->assertTrue( $record->hasRevision() );
- $this->assertTrue( $record->isInherited() );
- $this->assertSame( 'A', $record->getContent()->getNativeData() );
- $this->assertSame( 5, $record->getSize() );
- $this->assertSame( 'someHash', $record->getSha1() );
- $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
- $this->assertSame( 2, $record->getRevision() );
- $this->assertSame( 1, $record->getOrigin() );
- $this->assertSame( 'tt:456', $record->getAddress() );
- $this->assertSame( 33, $record->getContentId() );
- $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
- $this->assertSame( 'myRole', $record->getRole() );
- }
-
- public function testConstructionDeferred() {
- $row = $this->makeRow( [
- 'content_size' => null, // to be computed
- 'content_sha1' => null, // to be computed
- 'format_name' => function () {
- return CONTENT_FORMAT_WIKITEXT;
- },
- 'slot_revision_id' => '2',
- 'slot_origin' => '2',
- 'slot_content_id' => function () {
- return null;
- },
- ] );
-
- $content = function () {
- return new WikitextContent( 'A' );
- };
-
- $record = new SlotRecord( $row, $content );
-
- $this->assertTrue( $record->hasAddress() );
- $this->assertTrue( $record->hasRevision() );
- $this->assertFalse( $record->hasContentId() );
- $this->assertFalse( $record->isInherited() );
- $this->assertSame( 'A', $record->getContent()->getNativeData() );
- $this->assertSame( 1, $record->getSize() );
- $this->assertNotNull( $record->getSha1() );
- $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
- $this->assertSame( 2, $record->getRevision() );
- $this->assertSame( 2, $record->getRevision() );
- $this->assertSame( 'tt:456', $record->getAddress() );
- $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
- $this->assertSame( 'myRole', $record->getRole() );
- }
-
- public function testNewUnsaved() {
- $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
-
- $this->assertFalse( $record->hasAddress() );
- $this->assertFalse( $record->hasContentId() );
- $this->assertFalse( $record->hasRevision() );
- $this->assertFalse( $record->isInherited() );
- $this->assertFalse( $record->hasOrigin() );
- $this->assertSame( 'A', $record->getContent()->getNativeData() );
- $this->assertSame( 1, $record->getSize() );
- $this->assertNotNull( $record->getSha1() );
- $this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
- $this->assertSame( 'myRole', $record->getRole() );
- }
-
- public function provideInvalidConstruction() {
- yield 'both null' => [ null, null ];
- yield 'null row' => [ null, new WikitextContent( 'A' ) ];
- yield 'array row' => [ [], new WikitextContent( 'A' ) ];
- yield 'empty row' => [ (object)[], new WikitextContent( 'A' ) ];
- yield 'null content' => [ (object)[], null ];
- }
-
- /**
- * @dataProvider provideInvalidConstruction
- */
- public function testInvalidConstruction( $row, $content ) {
- $this->setExpectedException( InvalidArgumentException::class );
- new SlotRecord( $row, $content );
- }
-
- public function testGetContentId_fails() {
- $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $this->setExpectedException( IncompleteRevisionException::class );
-
- $record->getContentId();
- }
-
- public function testGetAddress_fails() {
- $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $this->setExpectedException( IncompleteRevisionException::class );
-
- $record->getAddress();
- }
-
- public function provideIncomplete() {
- $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- yield 'unsaved' => [ $unsaved ];
-
- $parent = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
- $inherited = SlotRecord::newInherited( $parent );
- yield 'inherited' => [ $inherited ];
- }
-
- /**
- * @dataProvider provideIncomplete
- */
- public function testGetRevision_fails( SlotRecord $record ) {
- $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $this->setExpectedException( IncompleteRevisionException::class );
-
- $record->getRevision();
- }
-
- /**
- * @dataProvider provideIncomplete
- */
- public function testGetOrigin_fails( SlotRecord $record ) {
- $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
- $this->setExpectedException( IncompleteRevisionException::class );
-
- $record->getOrigin();
- }
-
- public function provideHashStability() {
- yield [ '', 'phoiac9h4m842xq45sp7s6u21eteeq1' ];
- yield [ 'Lorem ipsum', 'hcr5u40uxr81d3nx89nvwzclfz6r9c5' ];
- }
-
- /**
- * @dataProvider provideHashStability
- */
- public function testHashStability( $text, $hash ) {
- // Changing the output of the hash function will break things horribly!
-
- $this->assertSame( $hash, SlotRecord::base36Sha1( $text ) );
-
- $record = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( $text ) );
- $this->assertSame( $hash, $record->getSha1() );
- }
-
- public function testNewWithSuppressedContent() {
- $input = new SlotRecord( $this->makeRow(), new WikitextContent( 'A' ) );
- $output = SlotRecord::newWithSuppressedContent( $input );
-
- $this->setExpectedException( SuppressedDataException::class );
- $output->getContent();
- }
-
- public function testNewInherited() {
- $row = $this->makeRow( [ 'slot_revision_id' => 7, 'slot_origin' => 7 ] );
- $parent = new SlotRecord( $row, new WikitextContent( 'A' ) );
-
- // This would happen while doing an edit, before saving revision meta-data.
- $inherited = SlotRecord::newInherited( $parent );
-
- $this->assertSame( $parent->getContentId(), $inherited->getContentId() );
- $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
- $this->assertSame( $parent->getContent(), $inherited->getContent() );
- $this->assertTrue( $inherited->isInherited() );
- $this->assertTrue( $inherited->hasOrigin() );
- $this->assertFalse( $inherited->hasRevision() );
-
- // make sure we didn't mess with the internal state of $parent
- $this->assertFalse( $parent->isInherited() );
- $this->assertSame( 7, $parent->getRevision() );
-
- // This would happen while doing an edit, after saving the revision meta-data
- // and content meta-data.
- $saved = SlotRecord::newSaved(
- 10,
- $inherited->getContentId(),
- $inherited->getAddress(),
- $inherited
- );
- $this->assertSame( $parent->getContentId(), $saved->getContentId() );
- $this->assertSame( $parent->getAddress(), $saved->getAddress() );
- $this->assertSame( $parent->getContent(), $saved->getContent() );
- $this->assertTrue( $saved->isInherited() );
- $this->assertTrue( $saved->hasRevision() );
- $this->assertSame( 10, $saved->getRevision() );
-
- // make sure we didn't mess with the internal state of $parent or $inherited
- $this->assertSame( 7, $parent->getRevision() );
- $this->assertFalse( $inherited->hasRevision() );
- }
-
- public function testNewSaved() {
- // This would happen while doing an edit, before saving revision meta-data.
- $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
- // This would happen while doing an edit, after saving the revision meta-data
- // and content meta-data.
- $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
- $this->assertFalse( $saved->isInherited() );
- $this->assertTrue( $saved->hasOrigin() );
- $this->assertTrue( $saved->hasRevision() );
- $this->assertTrue( $saved->hasAddress() );
- $this->assertTrue( $saved->hasContentId() );
- $this->assertSame( 'theNewAddress', $saved->getAddress() );
- $this->assertSame( 20, $saved->getContentId() );
- $this->assertSame( 'A', $saved->getContent()->getNativeData() );
- $this->assertSame( 10, $saved->getRevision() );
- $this->assertSame( 10, $saved->getOrigin() );
-
- // make sure we didn't mess with the internal state of $unsaved
- $this->assertFalse( $unsaved->hasAddress() );
- $this->assertFalse( $unsaved->hasContentId() );
- $this->assertFalse( $unsaved->hasRevision() );
- }
-
- public function provideNewSaved_LogicException() {
- $freshRow = $this->makeRow( [
- 'content_id' => 10,
- 'content_address' => 'address:1',
- 'slot_origin' => 1,
- 'slot_revision_id' => 1,
- ] );
-
- $freshSlot = new SlotRecord( $freshRow, new WikitextContent( 'A' ) );
- yield 'mismatching address' => [ 1, 10, 'address:BAD', $freshSlot ];
- yield 'mismatching revision' => [ 5, 10, 'address:1', $freshSlot ];
- yield 'mismatching content ID' => [ 1, 17, 'address:1', $freshSlot ];
-
- $inheritedRow = $this->makeRow( [
- 'content_id' => null,
- 'content_address' => null,
- 'slot_origin' => 0,
- 'slot_revision_id' => 1,
- ] );
-
- $inheritedSlot = new SlotRecord( $inheritedRow, new WikitextContent( 'A' ) );
- yield 'inherited, but no address' => [ 1, 10, 'address:2', $inheritedSlot ];
- }
-
- /**
- * @dataProvider provideNewSaved_LogicException
- */
- public function testNewSaved_LogicException(
- $revisionId,
- $contentId,
- $contentAddress,
- SlotRecord $protoSlot
- ) {
- $this->setExpectedException( LogicException::class );
- SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
- }
-
- public function provideNewSaved_InvalidArgumentException() {
- $unsaved = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
-
- yield 'bad revision id' => [ 'xyzzy', 5, 'address', $unsaved ];
- yield 'bad content id' => [ 7, 'xyzzy', 'address', $unsaved ];
- yield 'bad content address' => [ 7, 5, 77, $unsaved ];
- }
-
- /**
- * @dataProvider provideNewSaved_InvalidArgumentException
- */
- public function testNewSaved_InvalidArgumentException(
- $revisionId,
- $contentId,
- $contentAddress,
- SlotRecord $protoSlot
- ) {
- $this->setExpectedException( InvalidArgumentException::class );
- SlotRecord::newSaved( $revisionId, $contentId, $contentAddress, $protoSlot );
- }
-
- public function provideHasSameContent() {
- $fail = function () {
- self::fail( 'There should be no need to actually load the content.' );
- };
-
- $a100a1 = new SlotRecord(
- $this->makeRow(
- [
- 'model_name' => 'A',
- 'content_size' => 100,
- 'content_sha1' => 'hash-a',
- 'content_address' => 'xxx:a1',
- ]
- ),
- $fail
- );
- $a100a1b = new SlotRecord(
- $this->makeRow(
- [
- 'model_name' => 'A',
- 'content_size' => 100,
- 'content_sha1' => 'hash-a',
- 'content_address' => 'xxx:a1',
- ]
- ),
- $fail
- );
- $a100null = new SlotRecord(
- $this->makeRow(
- [
- 'model_name' => 'A',
- 'content_size' => 100,
- 'content_sha1' => 'hash-a',
- 'content_address' => null,
- ]
- ),
- $fail
- );
- $a100a2 = new SlotRecord(
- $this->makeRow(
- [
- 'model_name' => 'A',
- 'content_size' => 100,
- 'content_sha1' => 'hash-a',
- 'content_address' => 'xxx:a2',
- ]
- ),
- $fail
- );
- $b100a1 = new SlotRecord(
- $this->makeRow(
- [
- 'model_name' => 'B',
- 'content_size' => 100,
- 'content_sha1' => 'hash-a',
- 'content_address' => 'xxx:a1',
- ]
- ),
- $fail
- );
- $a200a1 = new SlotRecord(
- $this->makeRow(
- [
- 'model_name' => 'A',
- 'content_size' => 200,
- 'content_sha1' => 'hash-a',
- 'content_address' => 'xxx:a2',
- ]
- ),
- $fail
- );
- $a100x1 = new SlotRecord(
- $this->makeRow(
- [
- 'model_name' => 'A',
- 'content_size' => 100,
- 'content_sha1' => 'hash-x',
- 'content_address' => 'xxx:x1',
- ]
- ),
- $fail
- );
-
- yield 'same instance' => [ $a100a1, $a100a1, true ];
- yield 'no address' => [ $a100a1, $a100null, true ];
- yield 'same address' => [ $a100a1, $a100a1b, true ];
- yield 'different address' => [ $a100a1, $a100a2, true ];
- yield 'different model' => [ $a100a1, $b100a1, false ];
- yield 'different size' => [ $a100a1, $a200a1, false ];
- yield 'different hash' => [ $a100a1, $a100x1, false ];
- }
-
- /**
- * @dataProvider provideHasSameContent
- */
- public function testHasSameContent( SlotRecord $a, SlotRecord $b, $sameContent ) {
- $this->assertSame( $sameContent, $a->hasSameContent( $b ) );
- $this->assertSame( $sameContent, $b->hasSameContent( $a ) );
- }
-
-}
+++ /dev/null
-ALTER TABLE /*_*/revision ADD rev_text_id INTEGER DEFAULT 0;
-ALTER TABLE /*_*/revision ADD rev_content_model VARBINARY(32) DEFAULT NULL;
-ALTER TABLE /*_*/revision ADD rev_content_format VARBINARY(64) DEFAULT NULL;
+++ /dev/null
-DROP TABLE /*_*/slots;
-DROP TABLE /*_*/content;
-DROP TABLE /*_*/content_models;
-DROP TABLE /*_*/slot_roles;
+++ /dev/null
-ALTER TABLE /*_*/revision DROP COLUMN rev_text_id;
-ALTER TABLE /*_*/revision DROP COLUMN rev_content_model;
-ALTER TABLE /*_*/revision DROP COLUMN rev_content_format;
+++ /dev/null
-DROP TABLE /*_*/revision;
-
-CREATE TABLE /*_*/revision (
- rev_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- rev_page INTEGER NOT NULL,
- rev_comment BLOB NOT NULL,
- rev_user INTEGER NOT NULL default 0,
- rev_user_text varchar(255) NOT NULL default '',
- rev_timestamp blob(14) NOT NULL default '',
- rev_minor_edit INTEGER NOT NULL default 0,
- rev_deleted INTEGER NOT NULL default 0,
- rev_len INTEGER unsigned,
- rev_parent_id INTEGER default NULL,
- rev_sha1 varbinary(32) NOT NULL default ''
-) /*$wgDBTableOptions*/;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
/**
* @group medium
<?php
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Revision\SlotRenderingProvider;
-use MediaWiki\Storage\SlotRecord;
/**
* @group ContentHandler
<?php
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
use Wikimedia\TestingAccessWrapper;
/**
<?php
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
use PHPUnit\Framework\MockObject\MockObject;
/**
<?php
use MediaWiki\MediaWikiServices;
-use MediaWiki\Tests\Storage\McrSchemaOverride;
+use MediaWiki\Tests\Revision\McrSchemaOverride;
/**
* Test class for page archiving, using the new MCR schema.
<?php
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\SqlBlobStore;
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
/**
* Test class for page archiving, using the pre-MCR schema.
<?php
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
/**
* Base class for tests of PageArchive against different database schemas.
use MediaWiki\Edit\PreparedEdit;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\RevisionSlotsUpdate;
-use MediaWiki\Storage\SlotRecord;
use PHPUnit\Framework\MockObject\MockObject;
use Wikimedia\TestingAccessWrapper;
<?php
-use MediaWiki\Tests\Storage\McrSchemaOverride;
+use MediaWiki\Tests\Revision\McrSchemaOverride;
/**
* Tests WikiPage against the MCR DB schema after schema migration.
<?php
-use MediaWiki\Tests\Storage\McrReadNewSchemaOverride;
+use MediaWiki\Tests\Revision\McrReadNewSchemaOverride;
/**
* Tests WikiPage against the intermediate MCR DB schema for use during schema migration.
<?php
-use MediaWiki\Tests\Storage\McrWriteBothSchemaOverride;
+use MediaWiki\Tests\Revision\McrWriteBothSchemaOverride;
/**
* Tests WikiPage against the intermediate MCR DB schema for use during schema migration.
<?php
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
/**
* Tests WikiPage against the pre-MCR, pre ContentHandler DB schema.
<?php
-use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+use MediaWiki\Tests\Revision\PreMcrSchemaOverride;
/**
* Tests WikiPage against the pre-MCR DB schema.
<?php
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionStore;
+use MediaWiki\Revision\SlotRecord;
use MediaWiki\User\UserIdentityValue;
/**
<?php
use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Revision\MutableRevisionRecord;
+use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\SlotRecord;
/**
* @covers PoolWorkArticleView