*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
$this->config = $config;
}
+ /**
+ * @return RevisionStore
+ */
+ private function getRevisionStore() {
+ // TODO: Refactor: delete()/undelete() should live in a PageStore service;
+ // Methods in PageArchive and RevisionStore that deal with archive revisions
+ // should move into an ArchiveStore service (but could still be implemented
+ // together with RevisionStore).
+ return MediaWikiServices::getInstance()->getRevisionStore();
+ }
+
public function doesWrites() {
return true;
}
* wrapper with (ar_namespace, ar_title, count) fields, ordered by page
* namespace/title.
*
- * @return ResultWrapper
+ * @deprecated since 1.32.
+ *
+ * @return IResultWrapper
*/
public static function listAllPages() {
+ wfDeprecated( __METHOD__, '1.32' );
+
$dbr = wfGetDB( DB_REPLICA );
return self::listPages( $dbr, '' );
* Returns result wrapper with (ar_namespace, ar_title, count) fields.
*
* @param string $term Search term
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public static function listPagesBySearch( $term ) {
$title = Title::newFromText( $term );
* Returns result wrapper with (ar_namespace, ar_title, count) fields.
*
* @param string $prefix Title prefix
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public static function listPagesByPrefix( $prefix ) {
$dbr = wfGetDB( DB_REPLICA );
/**
* @param IDatabase $dbr
* @param string|array $condition
- * @return bool|ResultWrapper
+ * @return bool|IResultWrapper
*/
protected static function listPages( $dbr, $condition ) {
return $dbr->select(
* List the revisions of the given page. Returns result wrapper with
* various archive table fields.
*
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public function listRevisions() {
- $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $revisionStore = $this->getRevisionStore();
$queryInfo = $revisionStore->getArchiveQueryInfo();
$conds = [
'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey(),
];
- $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
+
+ // NOTE: ordering by ar_timestamp and ar_id, to remove ambiguity.
+ // XXX: Ideally, we would be ordering by ar_timestamp and ar_rev_id, but since we
+ // don't have an index on ar_rev_id, that causes a file sort.
+ $options = [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ];
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
* Returns a result wrapper with various filearchive fields, or null
* if not a file page.
*
- * @return ResultWrapper
+ * @return IResultWrapper
* @todo Does this belong in Image for fuller encapsulation?
*/
public function listFiles() {
/**
* Return a Revision object containing data for the deleted revision.
- * Note that the result *may* or *may not* have a null page ID.
+ *
+ * @deprecated since 1.32, use getArchivedRevision() instead.
*
* @param string $timestamp
* @return Revision|null
*/
public function getRevision( $timestamp ) {
$dbr = wfGetDB( DB_REPLICA );
- $arQuery = Revision::getArchiveQueryInfo();
+ $rec = $this->getRevisionByConditions(
+ [ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ]
+ );
+ return $rec ? new Revision( $rec ) : null;
+ }
+
+ /**
+ * Return the archived revision with the given ID.
+ *
+ * @param int $revId
+ * @return Revision|null
+ */
+ public function getArchivedRevision( $revId ) {
+ // Protect against code switching from getRevision() passing in a timestamp.
+ Assert::parameterType( 'integer', $revId, '$revId' );
+
+ $rec = $this->getRevisionByConditions( [ 'ar_rev_id' => $revId ] );
+ return $rec ? new Revision( $rec ) : null;
+ }
+
+ /**
+ * @param array $conditions
+ * @param array $options
+ *
+ * @return RevisionRecord|null
+ */
+ private function getRevisionByConditions( array $conditions, array $options = [] ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $arQuery = $this->getRevisionStore()->getArchiveQueryInfo();
+
+ $conditions = $conditions + [
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ ];
$row = $dbr->selectRow(
$arQuery['tables'],
$arQuery['fields'],
- [
- 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- 'ar_timestamp' => $dbr->timestamp( $timestamp )
- ],
+ $conditions,
__METHOD__,
- [],
+ $options,
$arQuery['joins']
);
if ( $row ) {
- return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
+ return $this->getRevisionStore()->newRevisionFromArchiveRow( $row, 0, $this->title );
}
return null;
// Check the previous deleted revision...
$row = $dbr->selectRow( 'archive',
- 'ar_timestamp',
+ [ 'ar_rev_id', 'ar_timestamp' ],
[ 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey(),
'ar_timestamp < ' .
'ORDER BY' => 'ar_timestamp DESC',
'LIMIT' => 1 ] );
$prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+ $prevDeletedId = $row ? intval( $row->ar_rev_id ) : null;
$row = $dbr->selectRow( [ 'page', 'revision' ],
[ 'rev_id', 'rev_timestamp' ],
if ( $prevLive && $prevLive > $prevDeleted ) {
// Most prior revision was live
- return Revision::newFromId( $prevLiveId );
+ $rec = $this->getRevisionStore()->getRevisionById( $prevLiveId );
+ $rec = $rec ? new Revision( $rec ) : null;
} elseif ( $prevDeleted ) {
// Most prior revision was deleted
- return $this->getRevision( $prevDeleted );
+ $rec = $this->getArchivedRevision( $prevDeletedId );
+ } else {
+ $rec = null;
}
- // No prior revision on this page.
- return null;
+ return $rec;
}
/**
- * Get the text from an archive row containing ar_text_id
+ * Get the text from an archive row containing ar_text_id.
+ *
+ * @deprecated since 1.32. In the MCR schema, ar_text_id no longer exists.
+ * Calling code should switch to getArchiveRevision().
+ *
+ * @todo remove in 1.33
*
- * @deprecated since 1.31
* @param object $row Database row
* @return string
*/
public function getTextFromRow( $row ) {
- $dbr = wfGetDB( DB_REPLICA );
- $text = $dbr->selectRow( 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $row->ar_text_id ],
- __METHOD__ );
+ wfDeprecated( __METHOD__, '1.32' );
+
+ if ( empty( $row->ar_text_id ) ) {
+ throw new InvalidArgumentException( '$row->ar_text_id must be set and not empty!' );
+ }
+
+ $address = SqlBlobStore::makeAddressFromTextId( $row->ar_text_id );
+ $blobStore = MediaWikiServices::getInstance()->getBlobStore();
- return Revision::getRevisionText( $text );
+ return $blobStore->getBlob( $address );
}
/**
*
* If there are no archived revisions for the page, returns NULL.
*
+ * @note this bypasses any audience checks.
+ *
+ * @deprecated since 1.32. For compatibility with the MCR schema,
+ * calling code should switch to getLastRevisionId() and getArchiveRevision().
+ *
+ * @todo remove in 1.33
+ *
* @return string|null
*/
public function getLastRevisionText() {
+ wfDeprecated( __METHOD__, '1.32' );
+
+ $revId = $this->getLastRevisionId();
+
+ if ( $revId ) {
+ $rev = $this->getArchivedRevision( $revId );
+ $content = $rev->getContent( RevisionRecord::RAW );
+ return $content->serialize();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the ID of the latest deleted revision.
+ *
+ * @return int|false The revision's ID, or false if there is no deleted revision.
+ */
+ public function getLastRevisionId() {
$dbr = wfGetDB( DB_REPLICA );
- $row = $dbr->selectRow(
- [ 'archive', 'text' ],
- [ 'old_text', 'old_flags' ],
+ $revId = $dbr->selectField(
+ 'archive',
+ 'ar_rev_id',
[ 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey() ],
__METHOD__,
- [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ],
- [ 'text' => [ 'JOIN', 'old_id = ar_text_id' ] ]
+ [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ]
);
- if ( $row ) {
- return Revision::getRevisionText( $row );
- }
-
- return null;
+ return $revId ? intval( $revId ) : false;
}
/**
* Quick check if any archived revisions are present for the page.
+ * This says nothing about whether the page currently exists in the page table or not.
*
* @return bool
*/
public function isDeleted() {
$dbr = wfGetDB( DB_REPLICA );
- $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+ $row = $dbr->selectRow(
+ [ 'archive' ],
+ '1', // We don't care about the value. Allow the database to optimize.
[ 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey() ],
__METHOD__
);
- return ( $n > 0 );
+ return (bool)$row;
}
/**
* @param string $comment
* @param array $fileVersions
* @param bool $unsuppress
- * @param User $user User performing the action, or null to use $wgUser
- * @param string|string[] $tags Change tags to add to log entry
+ * @param User|null $user User performing the action, or null to use $wgUser
+ * @param string|string[]|null $tags Change tags to add to log entry
* ($user should be able to add the specified tags before this is called)
* @return array|bool array(number of file revisions restored, number of image revisions
* restored, log message) on success, false on failure.
$oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
}
- $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $revisionStore = $this->getRevisionStore();
$queryInfo = $revisionStore->getArchiveQueryInfo();
$queryInfo['tables'][] = 'revision';
$queryInfo['fields'][] = 'rev_id';
if ( $latestRestorableRow !== null ) {
$oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
- // grab the content to check consistency with global state before restoring the page.
- $revision = Revision::newFromArchiveRow( $latestRestorableRow,
- [
- 'title' => $article->getTitle(), // used to derive default content model
- ]
+ // Grab the content to check consistency with global state before restoring the page.
+ // XXX: The only current use case is Wikibase, which tries to enforce uniqueness of
+ // certain things across all pages. There may be a better way to do that.
+ $revision = $revisionStore->newRevisionFromArchiveRow(
+ $latestRestorableRow,
+ 0,
+ $this->title
);
- $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
- $content = $revision->getContent( Revision::RAW );
- // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
- $status = $content->prepareSave( $article, 0, -1, $user );
- if ( !$status->isOK() ) {
- $dbw->endAtomic( __METHOD__ );
+ // TODO: use User::newFromUserIdentity from If610c68f4912e
+ // TODO: The User isn't used for anything in prepareSave()! We should drop it.
+ $user = User::newFromName( $revision->getUser( RevisionRecord::RAW )->getName(), false );
- return $status;
+ foreach ( $revision->getSlotRoles() as $role ) {
+ $content = $revision->getContent( $role, RevisionRecord::RAW );
+
+ // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
+ $status = $content->prepareSave( $article, 0, -1, $user );
+ if ( !$status->isOK() ) {
+ $dbw->endAtomic( __METHOD__ );
+
+ return $status;
+ }
}
}
$newid = false; // newly created page ID
$restored = 0; // number of revisions restored
- /** @var Revision $revision */
+ /** @var RevisionRecord|null $revision */
$revision = null;
$restoredPages = [];
// If there are no restorable revisions, we can skip most of the steps.
if ( $makepage ) {
// Check the state of the newest to-be version...
if ( !$unsuppress
- && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+ && ( $latestRestorableRow->ar_deleted & RevisionRecord::DELETED_TEXT )
) {
$dbw->endAtomic( __METHOD__ );
if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
// Check the state of the newest to-be version...
if ( !$unsuppress
- && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+ && ( $latestRestorableRow->ar_deleted & RevisionRecord::DELETED_TEXT )
) {
$dbw->endAtomic( __METHOD__ );
}
// Insert one revision at a time...maintaining deletion status
// unless we are specifically removing all restrictions...
- $revision = Revision::newFromArchiveRow( $row,
+ $revision = $revisionStore->newRevisionFromArchiveRow(
+ $row,
+ 0,
+ $this->title,
[
- 'page' => $pageId,
- 'title' => $this->title,
+ 'page_id' => $pageId,
'deleted' => $unsuppress ? 0 : $row->ar_deleted
- ] );
+ ]
+ );
// This will also copy the revision to ip_changes if it was an IP edit.
- $revision->insertOn( $dbw );
+ $revisionStore->insertRevisionOn( $revision, $dbw );
$restored++;
+ $legacyRevision = new Revision( $revision );
Hooks::run( 'ArticleRevisionUndeleted',
- [ &$this->title, $revision, $row->ar_page_id ] );
+ [ &$this->title, $legacyRevision, $row->ar_page_id ] );
$restoredPages[$row->ar_page_id] = true;
}
if ( $restored ) {
$created = (bool)$newid;
// Attach the latest revision to the page...
- $wasnew = $article->updateIfNewerOn( $dbw, $revision );
+ // XXX: updateRevisionOn should probably move into a PageStore service.
+ $wasnew = $article->updateIfNewerOn( $dbw, $legacyRevision );
if ( $created || $wasnew ) {
// Update site stats, link tables, etc
+ // TODO: use DerivedPageDataUpdater from If610c68f4912e!
$article->doEditUpdates(
- $revision,
- User::newFromName( $revision->getUserText( Revision::RAW ), false ),
+ $legacyRevision,
+ User::newFromName( $revision->getUser( RevisionRecord::RAW )->getName(), false ),
[
'created' => $created,
'oldcountable' => $oldcountable,