* @file
*/
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionAccessException;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\RevisionStoreRecord;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWiki\User\UserIdentityValue;
+use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\FakeResultWrapper;
/**
- * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
+ * @todo document
*/
class Revision implements IDBAccessObject {
-
- /** @var RevisionRecord */
- protected $mRecord;
+ /** @var int|null */
+ protected $mId;
+ /** @var int|null */
+ protected $mPage;
+ /** @var string */
+ protected $mUserText;
+ /** @var string */
+ protected $mOrigUserText;
+ /** @var int */
+ protected $mUser;
+ /** @var bool */
+ protected $mMinorEdit;
+ /** @var string */
+ protected $mTimestamp;
+ /** @var int */
+ protected $mDeleted;
+ /** @var int */
+ protected $mSize;
+ /** @var string */
+ protected $mSha1;
+ /** @var int */
+ protected $mParentId;
+ /** @var string */
+ protected $mComment;
+ /** @var string */
+ protected $mText;
+ /** @var int */
+ protected $mTextId;
+ /** @var int */
+ protected $mUnpatrolled;
+
+ /** @var stdClass|null */
+ protected $mTextRow;
+
+ /** @var null|Title */
+ protected $mTitle;
+ /** @var bool */
+ protected $mCurrent;
+ /** @var string */
+ protected $mContentModel;
+ /** @var string */
+ protected $mContentFormat;
+
+ /** @var Content|null|bool */
+ protected $mContent;
+ /** @var null|ContentHandler */
+ protected $mContentHandler;
+
+ /** @var int */
+ protected $mQueryFlags = 0;
+ /** @var bool Used for cached values to reload user text and rev_deleted */
+ protected $mRefreshMutableFields = false;
+ /** @var string Wiki ID; false means the current wiki */
+ protected $mWiki = false;
// Revision deletion constants
- const DELETED_TEXT = RevisionRecord::DELETED_TEXT;
- const DELETED_COMMENT = RevisionRecord::DELETED_COMMENT;
- const DELETED_USER = RevisionRecord::DELETED_USER;
- const DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED;
- const SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER;
- const SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL;
+ const DELETED_TEXT = 1;
+ const DELETED_COMMENT = 2;
+ const DELETED_USER = 4;
+ const DELETED_RESTRICTED = 8;
+ const SUPPRESSED_USER = 12; // convenience
+ const SUPPRESSED_ALL = 15; // convenience
// Audience options for accessors
- const FOR_PUBLIC = RevisionRecord::FOR_PUBLIC;
- const FOR_THIS_USER = RevisionRecord::FOR_THIS_USER;
- const RAW = RevisionRecord::RAW;
-
- const TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP;
+ const FOR_PUBLIC = 1;
+ const FOR_THIS_USER = 2;
+ const RAW = 3;
- /**
- * @return RevisionStore
- */
- protected static function getRevisionStore() {
- return MediaWikiServices::getInstance()->getRevisionStore();
- }
-
- /**
- * @return SqlBlobStore
- */
- protected static function getBlobStore() {
- $store = MediaWikiServices::getInstance()->getBlobStore();
-
- if ( !$store instanceof SqlBlobStore ) {
- throw new RuntimeException(
- 'The backwards compatibility code in Revision currently requires the BlobStore '
- . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store )
- );
- }
-
- return $store;
- }
+ const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
/**
* Load a page revision from a given revision ID number.
* @return Revision|null
*/
public static function newFromId( $id, $flags = 0 ) {
- $rec = self::getRevisionStore()->getRevisionById( $id, $flags );
- return $rec === null ? null : new Revision( $rec, $flags );
+ return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
}
/**
* @return Revision|null
*/
public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
- $rec = self::getRevisionStore()->getRevisionByTitle( $linkTarget, $id, $flags );
- return $rec === null ? null : new Revision( $rec, $flags );
+ $conds = [
+ 'page_namespace' => $linkTarget->getNamespace(),
+ 'page_title' => $linkTarget->getDBkey()
+ ];
+ if ( $id ) {
+ // Use the specified ID
+ $conds['rev_id'] = $id;
+ return self::newFromConds( $conds, $flags );
+ } else {
+ // Use a join to get the latest revision
+ $conds[] = 'rev_id=page_latest';
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+ return self::loadFromConds( $db, $conds, $flags );
+ }
}
/**
* @return Revision|null
*/
public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
- $rec = self::getRevisionStore()->getRevisionByPageId( $pageId, $revId, $flags );
- return $rec === null ? null : new Revision( $rec, $flags );
+ $conds = [ 'page_id' => $pageId ];
+ if ( $revId ) {
+ $conds['rev_id'] = $revId;
+ return self::newFromConds( $conds, $flags );
+ } else {
+ // Use a join to get the latest revision
+ $conds[] = 'rev_id = page_latest';
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+ return self::loadFromConds( $db, $conds, $flags );
+ }
}
/**
* Make a fake revision object from an archive table row. This is queried
* for permissions or even inserted (as in Special:Undelete)
+ * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
*
* @param object $row
* @param array $overrides
* @return Revision
*/
public static function newFromArchiveRow( $row, $overrides = [] ) {
- $rec = self::getRevisionStore()->newRevisionFromArchiveRow( $row, 0, null, $overrides );
- return new Revision( $rec );
+ global $wgContentHandlerUseDB;
+
+ $attribs = $overrides + [
+ 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
+ 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
+ 'comment' => CommentStore::newKey( 'ar_comment' )
+ // Legacy because $row may have come from self::selectArchiveFields()
+ ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
+ 'user' => $row->ar_user,
+ 'user_text' => $row->ar_user_text,
+ 'timestamp' => $row->ar_timestamp,
+ 'minor_edit' => $row->ar_minor_edit,
+ 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
+ 'deleted' => $row->ar_deleted,
+ 'len' => $row->ar_len,
+ 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
+ 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
+ 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
+ ];
+
+ if ( !$wgContentHandlerUseDB ) {
+ unset( $attribs['content_model'] );
+ unset( $attribs['content_format'] );
+ }
+
+ if ( !isset( $attribs['title'] )
+ && isset( $row->ar_namespace )
+ && isset( $row->ar_title )
+ ) {
+ $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+ }
+
+ if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
+ // Pre-1.5 ar_text row
+ $attribs['text'] = self::getRevisionText( $row, 'ar_' );
+ if ( $attribs['text'] === false ) {
+ throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
+ }
+ }
+ return new self( $attribs );
}
/**
* @since 1.19
*
- * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that
- * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead,
- * a MutableRevisionRecord should be constructed directly. RevisionStore::newRevisionFromArray()
- * can be used as a temporary replacement, but should be avoided.
- *
- * @param object|array $row
+ * @param object $row
* @return Revision
*/
public static function newFromRow( $row ) {
- if ( is_array( $row ) ) {
- $rec = self::getRevisionStore()->newMutableRevisionFromArray( $row );
- } else {
- $rec = self::getRevisionStore()->newRevisionFromRow( $row );
- }
-
- return new Revision( $rec );
+ return new self( $row );
}
/**
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
*
- * @deprecated since 1.31, use RevisionStore::getRevisionById() instead.
- *
* @param IDatabase $db
* @param int $id
* @return Revision|null
*/
public static function loadFromId( $db, $id ) {
- wfDeprecated( __METHOD__, '1.31' ); // no known callers
- $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
- return $rec === null ? null : new Revision( $rec );
+ return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
}
/**
* that's attached to a given page. If not attached
* to that page, will return null.
*
- * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead.
- *
* @param IDatabase $db
* @param int $pageid
* @param int $id
* @return Revision|null
*/
public static function loadFromPageId( $db, $pageid, $id = 0 ) {
- $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
- return $rec === null ? null : new Revision( $rec );
+ $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
+ if ( $id ) {
+ $conds['rev_id'] = intval( $id );
+ } else {
+ $conds[] = 'rev_id=page_latest';
+ }
+ return self::loadFromConds( $db, $conds );
}
/**
* that's attached to a given page. If not attached
* to that page, will return null.
*
- * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead.
- *
* @param IDatabase $db
* @param Title $title
* @param int $id
* @return Revision|null
*/
public static function loadFromTitle( $db, $title, $id = 0 ) {
- $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
- return $rec === null ? null : new Revision( $rec );
+ if ( $id ) {
+ $matchId = intval( $id );
+ } else {
+ $matchId = 'page_latest';
+ }
+ return self::loadFromConds( $db,
+ [
+ "rev_id=$matchId",
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ]
+ );
}
/**
* WARNING: Timestamps may in some circumstances not be unique,
* so this isn't the best key to use.
*
- * @deprecated since 1.31, use RevisionStore::loadRevisionFromTimestamp() instead.
- *
* @param IDatabase $db
* @param Title $title
* @param string $timestamp
* @return Revision|null
*/
public static function loadFromTimestamp( $db, $title, $timestamp ) {
- // XXX: replace loadRevisionFromTimestamp by getRevisionByTimestamp?
- $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
- return $rec === null ? null : new Revision( $rec );
+ return self::loadFromConds( $db,
+ [
+ 'rev_timestamp' => $db->timestamp( $timestamp ),
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ]
+ );
+ }
+
+ /**
+ * Given a set of conditions, fetch a revision
+ *
+ * This method is used then a revision ID is qualified and
+ * will incorporate some basic replica DB/master fallback logic
+ *
+ * @param array $conditions
+ * @param int $flags (optional)
+ * @return Revision|null
+ */
+ private static function newFromConds( $conditions, $flags = 0 ) {
+ $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
+
+ $rev = self::loadFromConds( $db, $conditions, $flags );
+ // 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 )
+ && wfGetLB()->getServerCount() > 1
+ && wfGetLB()->hasOrMadeRecentMasterChanges()
+ ) {
+ $flags = self::READ_LATEST;
+ $db = wfGetDB( DB_MASTER );
+ $rev = self::loadFromConds( $db, $conditions, $flags );
+ }
+
+ if ( $rev ) {
+ $rev->mQueryFlags = $flags;
+ }
+
+ return $rev;
+ }
+
+ /**
+ * Given a set of conditions, fetch a revision from
+ * the given database connection.
+ *
+ * @param IDatabase $db
+ * @param array $conditions
+ * @param int $flags (optional)
+ * @return Revision|null
+ */
+ private static function loadFromConds( $db, $conditions, $flags = 0 ) {
+ $row = self::fetchFromConds( $db, $conditions, $flags );
+ if ( $row ) {
+ $rev = new Revision( $row );
+ $rev->mWiki = $db->getDomainID();
+
+ return $rev;
+ }
+
+ return null;
}
/**
*
* @param LinkTarget $title
* @return ResultWrapper
- * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31.
+ * @deprecated Since 1.28
*/
public static function fetchRevision( LinkTarget $title ) {
- wfDeprecated( __METHOD__, '1.31' );
- return new FakeResultWrapper( [] );
+ $row = self::fetchFromConds(
+ wfGetDB( DB_REPLICA ),
+ [
+ 'rev_id=page_latest',
+ 'page_namespace' => $title->getNamespace(),
+ 'page_title' => $title->getDBkey()
+ ]
+ );
+
+ return new FakeResultWrapper( $row ? [ $row ] : [] );
+ }
+
+ /**
+ * Given a set of conditions, return a ResultWrapper
+ * which will return matching database rows with the
+ * fields necessary to build Revision objects.
+ *
+ * @param IDatabase $db
+ * @param array $conditions
+ * @param int $flags (optional)
+ * @return stdClass
+ */
+ private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
+ $revQuery = self::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']
+ );
}
/**
* Return the value of a select() JOIN conds array for the user table.
* This will get user table rows for logged-in users.
* @since 1.19
- * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
+ * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function userJoinCond() {
* Return the value of a select() page conds array for the page table.
* This will assure that the revision(s) are not orphaned from live pages.
* @since 1.19
- * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
+ * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function pageJoinCond() {
/**
* Return the list of revision fields that should be selected to create
* a new revision.
- * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
+ * @deprecated since 1.31, use self::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
/**
* Return the list of revision fields that should be selected to create
* a new revision from an archive row.
- * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
+ * @deprecated since 1.31, use self::getArchiveQueryInfo() instead.
* @return array
*/
public static function selectArchiveFields() {
/**
* Return the list of text fields that should be selected to read the
* revision text
- * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead.
+ * @deprecated since 1.31, use self::getQueryInfo( [ 'text' ] ) instead.
* @return array
*/
public static function selectTextFields() {
/**
* Return the list of page fields that should be selected from page table
- * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
+ * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function selectPageFields() {
/**
* Return the list of user fields that should be selected from user table
- * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
+ * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function selectUserFields() {
* Return the tables, fields, and join conditions to be selected to create
* a new revision object.
* @since 1.31
- * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
* @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
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
*/
public static function getQueryInfo( $options = [] ) {
- return self::getRevisionStore()->getQueryInfo( $options );
+ global $wgContentHandlerUseDB;
+
+ $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin();
+ $ret = [
+ 'tables' => [ 'revision' ] + $commentQuery['tables'],
+ 'fields' => [
+ 'rev_id',
+ 'rev_page',
+ 'rev_text_id',
+ 'rev_timestamp',
+ 'rev_user_text',
+ 'rev_user',
+ 'rev_minor_edit',
+ 'rev_deleted',
+ 'rev_len',
+ 'rev_parent_id',
+ 'rev_sha1',
+ ] + $commentQuery['fields'],
+ 'joins' => $commentQuery['joins'],
+ ];
+
+ if ( $wgContentHandlerUseDB ) {
+ $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',
+ ] );
+ $ret['joins']['user'] = [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
+ }
+
+ if ( in_array( 'text', $options, true ) ) {
+ $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 archived revision object.
* @since 1.31
- * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
* @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 static function getArchiveQueryInfo() {
- return self::getRevisionStore()->getArchiveQueryInfo();
+ global $wgContentHandlerUseDB;
+
+ $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
+ $ret = [
+ 'tables' => [ 'archive' ] + $commentQuery['tables'],
+ 'fields' => [
+ 'ar_id',
+ 'ar_page_id',
+ 'ar_rev_id',
+ 'ar_text',
+ 'ar_text_id',
+ 'ar_timestamp',
+ 'ar_user_text',
+ 'ar_user',
+ 'ar_minor_edit',
+ 'ar_deleted',
+ 'ar_len',
+ 'ar_parent_id',
+ 'ar_sha1',
+ ] + $commentQuery['fields'],
+ 'joins' => $commentQuery['joins'],
+ ];
+
+ if ( $wgContentHandlerUseDB ) {
+ $ret['fields'][] = 'ar_content_format';
+ $ret['fields'][] = 'ar_content_model';
+ }
+
+ return $ret;
}
/**
* @return array
*/
public static function getParentLengths( $db, array $revIds ) {
- return self::getRevisionStore()->listRevisionSizes( $db, $revIds );
+ $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] = $row->rev_len;
+ }
+ return $revLens;
}
/**
- * @param object|array|RevisionRecord $row Either a database row or an array
- * @param int $queryFlags
- * @param Title|null $title
- *
+ * @param object|array $row Either a database row or an array
+ * @throws MWException
* @access private
*/
- function __construct( $row, $queryFlags = 0, Title $title = null ) {
- global $wgUser;
-
- if ( $row instanceof RevisionRecord ) {
- $this->mRecord = $row;
+ public function __construct( $row ) {
+ if ( is_object( $row ) ) {
+ $this->constructFromDbRowObject( $row );
} elseif ( is_array( $row ) ) {
- if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) {
- $row['user'] = $wgUser;
- }
+ $this->constructFromRowArray( $row );
+ } else {
+ throw new MWException( 'Revision constructor passed invalid row format.' );
+ }
+ $this->mUnpatrolled = null;
+ }
- $this->mRecord = self::getRevisionStore()->newMutableRevisionFromArray(
- $row,
- $queryFlags,
- $title
- );
- } elseif ( is_object( $row ) ) {
- $this->mRecord = self::getRevisionStore()->newRevisionFromRow(
- $row,
- $queryFlags,
- $title
- );
+ /**
+ * @param object $row
+ */
+ private function constructFromDbRowObject( $row ) {
+ $this->mId = intval( $row->rev_id );
+ $this->mPage = intval( $row->rev_page );
+ $this->mTextId = intval( $row->rev_text_id );
+ $this->mComment = CommentStore::newKey( 'rev_comment' )
+ // Legacy because $row may have come from self::selectFields()
+ ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
+ $this->mUser = intval( $row->rev_user );
+ $this->mMinorEdit = intval( $row->rev_minor_edit );
+ $this->mTimestamp = $row->rev_timestamp;
+ $this->mDeleted = intval( $row->rev_deleted );
+
+ if ( !isset( $row->rev_parent_id ) ) {
+ $this->mParentId = null;
} else {
- throw new InvalidArgumentException(
- '$row must be a row object, an associative array, or a RevisionRecord'
- );
+ $this->mParentId = intval( $row->rev_parent_id );
}
+
+ if ( !isset( $row->rev_len ) ) {
+ $this->mSize = null;
+ } else {
+ $this->mSize = intval( $row->rev_len );
+ }
+
+ if ( !isset( $row->rev_sha1 ) ) {
+ $this->mSha1 = null;
+ } else {
+ $this->mSha1 = $row->rev_sha1;
+ }
+
+ if ( isset( $row->page_latest ) ) {
+ $this->mCurrent = ( $row->rev_id == $row->page_latest );
+ $this->mTitle = Title::newFromRow( $row );
+ } else {
+ $this->mCurrent = false;
+ $this->mTitle = null;
+ }
+
+ if ( !isset( $row->rev_content_model ) ) {
+ $this->mContentModel = null; # determine on demand if needed
+ } else {
+ $this->mContentModel = strval( $row->rev_content_model );
+ }
+
+ if ( !isset( $row->rev_content_format ) ) {
+ $this->mContentFormat = null; # determine on demand if needed
+ } else {
+ $this->mContentFormat = strval( $row->rev_content_format );
+ }
+
+ // Lazy extraction...
+ $this->mText = null;
+ if ( isset( $row->old_text ) ) {
+ $this->mTextRow = $row;
+ } else {
+ // 'text' table row entry will be lazy-loaded
+ $this->mTextRow = null;
+ }
+
+ // Use user_name for users and rev_user_text for IPs...
+ $this->mUserText = null; // lazy load if left null
+ if ( $this->mUser == 0 ) {
+ $this->mUserText = $row->rev_user_text; // IP user
+ } elseif ( isset( $row->user_name ) ) {
+ $this->mUserText = $row->user_name; // logged-in user
+ }
+ $this->mOrigUserText = $row->rev_user_text;
}
/**
- * @return RevisionRecord
+ * @param array $row
+ *
+ * @throws MWException
*/
- public function getRevisionRecord() {
- return $this->mRecord;
+ private function constructFromRowArray( array $row ) {
+ // Build a new revision to be saved...
+ global $wgUser; // ugh
+
+ # if we have a content object, use it to set the model and type
+ if ( !empty( $row['content'] ) ) {
+ if ( !( $row['content'] instanceof Content ) ) {
+ throw new MWException( '`content` field must contain a Content object.' );
+ }
+
+ // @todo when is that set? test with external store setup! check out insertOn() [dk]
+ if ( !empty( $row['text_id'] ) ) {
+ throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
+ "can't serialize content object" );
+ }
+
+ $row['content_model'] = $row['content']->getModel();
+ # note: mContentFormat is initializes later accordingly
+ # note: content is serialized later in this method!
+ # also set text to null?
+ }
+
+ $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
+ $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
+ $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
+ $this->mUserText = isset( $row['user_text'] )
+ ? strval( $row['user_text'] ) : $wgUser->getName();
+ $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
+ $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
+ $this->mTimestamp = isset( $row['timestamp'] )
+ ? strval( $row['timestamp'] ) : wfTimestampNow();
+ $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
+ $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
+ $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
+ $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
+
+ $this->mContentModel = isset( $row['content_model'] )
+ ? strval( $row['content_model'] ) : null;
+ $this->mContentFormat = isset( $row['content_format'] )
+ ? strval( $row['content_format'] ) : null;
+
+ // Enforce spacing trimming on supplied text
+ $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
+ $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
+ $this->mTextRow = null;
+
+ $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
+
+ // if we have a Content object, override mText and mContentModel
+ if ( !empty( $row['content'] ) ) {
+ $handler = $this->getContentHandler();
+ $this->mContent = $row['content'];
+
+ $this->mContentModel = $this->mContent->getModel();
+ $this->mContentHandler = null;
+
+ $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
+ } elseif ( $this->mText !== null ) {
+ $handler = $this->getContentHandler();
+ $this->mContent = $handler->unserializeContent( $this->mText );
+ }
+
+ // If we have a Title object, make sure it is consistent with mPage.
+ if ( $this->mTitle && $this->mTitle->exists() ) {
+ if ( $this->mPage === null ) {
+ // if the page ID wasn't known, set it now
+ $this->mPage = $this->mTitle->getArticleID();
+ } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
+ // Got different page IDs. This may be legit (e.g. during undeletion),
+ // but it seems worth mentioning it in the log.
+ wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
+ $this->mTitle->getArticleID() . " provided by the Title object." );
+ }
+ }
+
+ $this->mCurrent = false;
+
+ // If we still have no length, see it we have the text to figure it out
+ if ( !$this->mSize && $this->mContent !== null ) {
+ $this->mSize = $this->mContent->getSize();
+ }
+
+ // Same for sha1
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
+ }
+
+ // force lazy init
+ $this->getContentModel();
+ $this->getContentFormat();
}
/**
* @return int|null
*/
public function getId() {
- return $this->mRecord->getId();
+ return $this->mId;
}
/**
* Set the revision ID
*
- * This should only be used for proposed revisions that turn out to be null edits.
- *
- * @note Only supported on Revisions that were constructed based on associative arrays,
- * since they are mutable.
+ * This should only be used for proposed revisions that turn out to be null edits
*
* @since 1.19
- * @param int|string $id
- * @throws MWException
+ * @param int $id
*/
public function setId( $id ) {
- if ( $this->mRecord instanceof MutableRevisionRecord ) {
- $this->mRecord->setId( intval( $id ) );
- } else {
- throw new MWException( __METHOD__ . ' is not supported on this instance' );
- }
+ $this->mId = (int)$id;
}
/**
*
* This should only be used for proposed revisions that turn out to be null edits
*
- * @note Only supported on Revisions that were constructed based on associative arrays,
- * since they are mutable.
- *
* @since 1.28
* @deprecated since 1.31, please reuse old Revision object
* @param int $id User ID
* @param string $name User name
- * @throws MWException
*/
public function setUserIdAndName( $id, $name ) {
- if ( $this->mRecord instanceof MutableRevisionRecord ) {
- $user = new UserIdentityValue( intval( $id ), $name );
- $this->mRecord->setUser( $user );
- } else {
- throw new MWException( __METHOD__ . ' is not supported on this instance' );
- }
+ $this->mUser = (int)$id;
+ $this->mUserText = $name;
+ $this->mOrigUserText = $name;
}
/**
- * @return SlotRecord
- */
- private function getMainSlotRaw() {
- return $this->mRecord->getSlot( 'main', RevisionRecord::RAW );
- }
-
- /**
- * Get the ID of the row of the text table that contains the content of the
- * revision's main slot, if that content is stored in the text table.
- *
- * If the content is stored elsewhere, this returns null.
- *
- * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to
- * get that actual address that can be used with BlobStore::getBlob(); or use
- * RevisionRecord::hasSameContent() to check if two revisions have the same content.
+ * Get text row ID
*
* @return int|null
*/
public function getTextId() {
- $slot = $this->getMainSlotRaw();
- return $slot->hasAddress()
- ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() )
- : null;
+ return $this->mTextId;
}
/**
* Get parent revision ID (the original previous page revision)
*
- * @return int|null The ID of the parent revision. 0 indicates that there is no
- * parent revision. Null indicates that the parent revision is not known.
+ * @return int|null
*/
public function getParentId() {
- return $this->mRecord->getParentId();
+ return $this->mParentId;
}
/**
* Returns the length of the text in this revision, or null if unknown.
*
- * @return int
+ * @return int|null
*/
public function getSize() {
- return $this->mRecord->getSize();
+ return $this->mSize;
}
/**
- * Returns the base36 sha1 of the content in this revision, or null if unknown.
+ * Returns the base36 sha1 of the text in this revision, or null if unknown.
*
- * @return string
+ * @return string|null
*/
public function getSha1() {
- // XXX: we may want to drop all the hashing logic, it's not worth the overhead.
- return $this->mRecord->getSha1();
+ return $this->mSha1;
}
/**
- * Returns the title of the page associated with this entry.
- * Since 1.31, this will never return null.
+ * Returns the title of the page associated with this entry or null.
*
* Will do a query, when title is not set and id is given.
*
- * @return Title
+ * @return Title|null
*/
public function getTitle() {
- $linkTarget = $this->mRecord->getPageAsLinkTarget();
- return Title::newFromLinkTarget( $linkTarget );
+ if ( $this->mTitle !== null ) {
+ return $this->mTitle;
+ }
+ // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
+ if ( $this->mId !== null ) {
+ $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
+ // @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' => $this->mId ],
+ __METHOD__,
+ [],
+ [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
+ );
+ if ( $row ) {
+ // @TODO: better foreign title handling
+ $this->mTitle = Title::newFromRow( $row );
+ }
+ }
+
+ if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
+ // Loading by ID is best, though not possible for foreign titles
+ if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
+ $this->mTitle = Title::newFromID( $this->mPage );
+ }
+ }
+
+ return $this->mTitle;
}
/**
* Set the title of the revision
*
- * @deprecated: since 1.31, this is now a noop. Pass the Title to the constructor instead.
- *
* @param Title $title
*/
public function setTitle( $title ) {
- if ( !$title->equals( $this->getTitle() ) ) {
- throw new InvalidArgumentException(
- $title->getPrefixedText()
- . ' is not the same as '
- . $this->mRecord->getPageAsLinkTarget()->__toString()
- );
- }
+ $this->mTitle = $title;
}
/**
* @return int|null
*/
public function getPage() {
- return $this->mRecord->getPageId();
+ return $this->mPage;
}
/**
* @return int
*/
public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- global $wgUser;
-
- if ( $audience === self::FOR_THIS_USER && !$user ) {
- $user = $wgUser;
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+ return 0;
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
+ return 0;
+ } else {
+ return $this->mUser;
}
-
- $user = $this->mRecord->getUser( $audience, $user );
- return $user ? $user->getId() : 0;
}
/**
* @return string
*/
public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
- global $wgUser;
+ $this->loadMutableFields();
- if ( $audience === self::FOR_THIS_USER && !$user ) {
- $user = $wgUser;
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
+ return '';
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
+ return '';
+ } else {
+ if ( $this->mUserText === null ) {
+ $this->mUserText = User::whoIs( $this->mUser ); // load on demand
+ if ( $this->mUserText === false ) {
+ # This shouldn't happen, but it can if the wiki was recovered
+ # via importing revs and there is no user table entry yet.
+ $this->mUserText = $this->mOrigUserText;
+ }
+ }
+ return $this->mUserText;
}
-
- $user = $this->mRecord->getUser( $audience, $user );
- return $user ? $user->getName() : '';
}
/**
* @return string
*/
function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- global $wgUser;
-
- if ( $audience === self::FOR_THIS_USER && !$user ) {
- $user = $wgUser;
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
+ return '';
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
+ return '';
+ } else {
+ return $this->mComment;
}
-
- $comment = $this->mRecord->getComment( $audience, $user );
- return $comment === null ? null : $comment->text;
}
/**
* @return bool
*/
public function isMinor() {
- return $this->mRecord->isMinor();
+ return (bool)$this->mMinorEdit;
}
/**
* @return int Rcid of the unpatrolled row, zero if there isn't one
*/
public function isUnpatrolled() {
- return self::getRevisionStore()->isUnpatrolled( $this->mRecord );
+ if ( $this->mUnpatrolled !== null ) {
+ return $this->mUnpatrolled;
+ }
+ $rc = $this->getRecentChange();
+ if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
+ $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
+ } else {
+ $this->mUnpatrolled = 0;
+ }
+ return $this->mUnpatrolled;
}
/**
* @return RecentChange|null
*/
public function getRecentChange( $flags = 0 ) {
- return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags );
+ $dbr = wfGetDB( DB_REPLICA );
+
+ list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
+
+ return RecentChange::newFromConds(
+ [
+ 'rc_user_text' => $this->getUserText( self::RAW ),
+ 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
+ 'rc_this_oldid' => $this->getId()
+ ],
+ __METHOD__,
+ $dbType
+ );
}
/**
* @return bool
*/
public function isDeleted( $field ) {
- return $this->mRecord->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 ( $this->getVisibility() & $field ) == $field;
}
/**
* @return int
*/
public function getVisibility() {
- return $this->mRecord->getVisibility();
+ $this->loadMutableFields();
+
+ return (int)$this->mDeleted;
}
/**
* Fetch revision content if it's available to the specified audience.
* If the specified audience does not have the ability to view this
- * revision, or the content could not be loaded, null will be returned.
+ * revision, null will be returned.
*
* @param int $audience One of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $user
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the text regardless of permissions
* @param User $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return Content|null
*/
public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
- global $wgUser;
-
- if ( $audience === self::FOR_THIS_USER && !$user ) {
- $user = $wgUser;
- }
-
- try {
- return $this->mRecord->getContent( 'main', $audience, $user );
- }
- catch ( RevisionAccessException $e ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
+ return null;
+ } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
return null;
+ } else {
+ return $this->getContentInternal();
}
}
* Get original serialized data (without checking view restrictions)
*
* @since 1.21
- * @deprecated since 1.31, use BlobStore::getBlob instead.
- *
* @return string
*/
public function getSerializedData() {
- $slot = $this->getMainSlotRaw();
- return $slot->getContent()->serialize();
+ if ( $this->mText === null ) {
+ // Revision is immutable. Load on demand.
+ $this->mText = $this->loadText();
+ }
+
+ return $this->mText;
}
/**
- * Returns the content model for the main slot of this revision.
+ * Gets the content object for the revision (or null on failure).
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * @since 1.21
+ * @return Content|null The Revision's content, or null on failure.
+ */
+ protected function getContentInternal() {
+ if ( $this->mContent === null ) {
+ $text = $this->getSerializedData();
+
+ if ( $text !== null && $text !== false ) {
+ // Unserialize content
+ $handler = $this->getContentHandler();
+ $format = $this->getContentFormat();
+
+ $this->mContent = $handler->unserializeContent( $text, $format );
+ }
+ }
+
+ // NOTE: copy() will return $this for immutable content objects
+ return $this->mContent ? $this->mContent->copy() : null;
+ }
+
+ /**
+ * Returns the content model for this revision.
*
* If no content model was stored in the database, the default content model for the title is
* used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
* is used as a last resort.
*
- * @todo: drop this, with MCR, there no longer is a single model associated with a revision.
- *
* @return string The content model id associated with this revision,
* see the CONTENT_MODEL_XXX constants.
*/
public function getContentModel() {
- return $this->getMainSlotRaw()->getModel();
+ if ( !$this->mContentModel ) {
+ $title = $this->getTitle();
+ if ( $title ) {
+ $this->mContentModel = ContentHandler::getDefaultModelFor( $title );
+ } else {
+ $this->mContentModel = CONTENT_MODEL_WIKITEXT;
+ }
+
+ assert( !empty( $this->mContentModel ) );
+ }
+
+ return $this->mContentModel;
}
/**
- * Returns the content format for the main slot of this revision.
+ * Returns the content format for this revision.
*
* If no content format was stored in the database, the default format for this
* revision's content model is returned.
*
- * @todo: drop this, the format is irrelevant to the revision!
- *
* @return string The content format id associated with this revision,
* see the CONTENT_FORMAT_XXX constants.
*/
public function getContentFormat() {
- $format = $this->getMainSlotRaw()->getFormat();
+ if ( !$this->mContentFormat ) {
+ $handler = $this->getContentHandler();
+ $this->mContentFormat = $handler->getDefaultFormat();
- if ( $format === null ) {
- // if no format was stored along with the blob, fall back to default format
- $format = $this->getContentHandler()->getDefaultFormat();
+ assert( !empty( $this->mContentFormat ) );
}
- return $format;
+ return $this->mContentFormat;
}
/**
* @return ContentHandler
*/
public function getContentHandler() {
- return ContentHandler::getForModelID( $this->getContentModel() );
+ if ( !$this->mContentHandler ) {
+ $model = $this->getContentModel();
+ $this->mContentHandler = ContentHandler::getForModelID( $model );
+
+ $format = $this->getContentFormat();
+
+ if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
+ throw new MWException( "Oops, the content format $format is not supported for "
+ . "this content model, $model" );
+ }
+ }
+
+ return $this->mContentHandler;
}
/**
* @return string
*/
public function getTimestamp() {
- return $this->mRecord->getTimestamp();
+ return wfTimestamp( TS_MW, $this->mTimestamp );
}
/**
* @return bool
*/
public function isCurrent() {
- return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent();
+ return $this->mCurrent;
}
/**
* @return Revision|null
*/
public function getPrevious() {
- $rec = self::getRevisionStore()->getPreviousRevision( $this->mRecord );
- return $rec === null ? null : new Revision( $rec );
+ if ( $this->getTitle() ) {
+ $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
+ if ( $prev ) {
+ return self::newFromTitle( $this->getTitle(), $prev );
+ }
+ }
+ return null;
}
/**
* @return Revision|null
*/
public function getNext() {
- $rec = self::getRevisionStore()->getNextRevision( $this->mRecord );
- return $rec === null ? null : new Revision( $rec );
+ if ( $this->getTitle() ) {
+ $next = $this->getTitle()->getNextRevisionID( $this->getId() );
+ if ( $next ) {
+ return self::newFromTitle( $this->getTitle(), $next );
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get previous revision Id for this page_id
+ * This is used to populate rev_parent_id on save
+ *
+ * @param IDatabase $db
+ * @return int
+ */
+ private function getPreviousRevisionId( $db ) {
+ if ( $this->mPage === null ) {
+ return 0;
+ }
+ # Use page_latest if ID is not given
+ if ( !$this->mId ) {
+ $prevId = $db->selectField( 'page', 'page_latest',
+ [ 'page_id' => $this->mPage ],
+ __METHOD__ );
+ } else {
+ $prevId = $db->selectField( 'revision', 'rev_id',
+ [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_id DESC' ] );
+ }
+ return intval( $prevId );
}
/**
return false;
}
- $cacheKey = isset( $row->old_id ) ? ( 'tt:' . $row->old_id ) : null;
+ // Use external methods for external objects, text in table is URL-only then
+ if ( in_array( 'external', $flags ) ) {
+ $url = $text;
+ $parts = explode( '://', $url, 2 );
+ if ( count( $parts ) == 1 || $parts[1] == '' ) {
+ return false;
+ }
+
+ if ( isset( $row->old_id ) && $wiki === false ) {
+ // Make use of the wiki-local revision text cache
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ // The cached value should be decompressed, so handle that and return here
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'revisiontext', 'textid', $row->old_id ),
+ self::getCacheTTL( $cache ),
+ function () use ( $url, $wiki, $flags ) {
+ // No negative caching per Revision::loadText()
+ $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
+
+ return self::decompressRevisionText( $text, $flags );
+ },
+ [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
+ );
+ } else {
+ $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
+ }
+ }
- return self::getBlobStore()->expandBlob( $text, $flags, $cacheKey );
+ return self::decompressRevisionText( $text, $flags );
}
/**
* @return string
*/
public static function compressRevisionText( &$text ) {
- return self::getBlobStore()->compressData( $text );
+ global $wgCompressRevisions;
+ $flags = [];
+
+ # Revisions not marked this way will be converted
+ # on load if $wgLegacyCharset is set in the future.
+ $flags[] = 'utf-8';
+
+ if ( $wgCompressRevisions ) {
+ if ( function_exists( 'gzdeflate' ) ) {
+ $deflated = gzdeflate( $text );
+
+ if ( $deflated === false ) {
+ wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
+ } else {
+ $text = $deflated;
+ $flags[] = 'gzip';
+ }
+ } else {
+ wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
+ }
+ }
+ return implode( ',', $flags );
}
/**
* @return string|bool Decompressed text, or false on failure
*/
public static function decompressRevisionText( $text, $flags ) {
- return self::getBlobStore()->decompressData( $text, $flags );
+ global $wgLegacyEncoding, $wgContLang;
+
+ if ( $text === false ) {
+ // Text failed to be fetched; nothing to do
+ return false;
+ }
+
+ if ( in_array( 'gzip', $flags ) ) {
+ # Deal with optional compression of archived pages.
+ # This can be done periodically via maintenance/compressOld.php, and
+ # as pages are saved if $wgCompressRevisions is set.
+ $text = gzinflate( $text );
+
+ if ( $text === false ) {
+ wfLogWarning( __METHOD__ . ': gzinflate() failed' );
+ return false;
+ }
+ }
+
+ if ( in_array( 'object', $flags ) ) {
+ # Generic compressed storage
+ $obj = unserialize( $text );
+ if ( !is_object( $obj ) ) {
+ // Invalid object
+ return false;
+ }
+ $text = $obj->getText();
+ }
+
+ if ( $text !== false && $wgLegacyEncoding
+ && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
+ ) {
+ # Old revisions kept around in a legacy encoding?
+ # Upconvert on demand.
+ # ("utf8" checked for compatibility with some broken
+ # conversion scripts 2008-12-30)
+ $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
+ }
+
+ return $text;
}
/**
* @return int The revision ID
*/
public function insertOn( $dbw ) {
- global $wgUser;
+ global $wgDefaultExternalStore, $wgContentHandlerUseDB;
+
+ // We're inserting a new revision, so we have to use master anyway.
+ // If it's a null revision, it may have references to rows that
+ // are not in the replica yet (the text row).
+ $this->mQueryFlags |= self::READ_LATEST;
+
+ // Not allowed to have rev_page equal to 0, false, etc.
+ if ( !$this->mPage ) {
+ $title = $this->getTitle();
+ if ( $title instanceof Title ) {
+ $titleText = ' for page ' . $title->getPrefixedText();
+ } else {
+ $titleText = '';
+ }
+ throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
+ }
- // Note that $this->mRecord->getId() will typically return null here, but not always,
- // e.g. not when restoring a revision.
+ $this->checkContentModel();
- if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) {
- if ( $this->mRecord instanceof MutableRevisionRecord ) {
- $this->mRecord->setUser( $wgUser );
- } else {
- throw new MWException( 'Cannot insert revision with no associated user.' );
+ $data = $this->mText;
+ $flags = self::compressRevisionText( $data );
+
+ # Write to external storage if required
+ if ( $wgDefaultExternalStore ) {
+ // Store and get the URL
+ $data = ExternalStore::insertToDefault( $data );
+ if ( !$data ) {
+ throw new MWException( "Unable to store text to external storage" );
+ }
+ if ( $flags ) {
+ $flags .= ',';
+ }
+ $flags .= 'external';
+ }
+
+ # Record the text (or external storage URL) to the text table
+ if ( $this->mTextId === null ) {
+ $dbw->insert( 'text',
+ [
+ 'old_text' => $data,
+ 'old_flags' => $flags,
+ ], __METHOD__
+ );
+ $this->mTextId = $dbw->insertId();
+ }
+
+ if ( $this->mComment === null ) {
+ $this->mComment = "";
+ }
+
+ # Record the edit in revisions
+ $row = [
+ 'rev_page' => $this->mPage,
+ 'rev_text_id' => $this->mTextId,
+ 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
+ 'rev_user' => $this->mUser,
+ 'rev_user_text' => $this->mUserText,
+ 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
+ 'rev_deleted' => $this->mDeleted,
+ 'rev_len' => $this->mSize,
+ 'rev_parent_id' => $this->mParentId === null
+ ? $this->getPreviousRevisionId( $dbw )
+ : $this->mParentId,
+ 'rev_sha1' => $this->mSha1 === null
+ ? self::base36Sha1( $this->mText )
+ : $this->mSha1,
+ ];
+ if ( $this->mId !== null ) {
+ $row['rev_id'] = $this->mId;
+ }
+
+ list( $commentFields, $commentCallback ) =
+ CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
+ $row += $commentFields;
+
+ if ( $wgContentHandlerUseDB ) {
+ // NOTE: Store null for the default model and format, to save space.
+ // XXX: Makes the DB sensitive to changed defaults.
+ // Make this behavior optional? Only in miser mode?
+
+ $model = $this->getContentModel();
+ $format = $this->getContentFormat();
+
+ $title = $this->getTitle();
+
+ if ( $title === null ) {
+ throw new MWException( "Insufficient information to determine the title of the "
+ . "revision's page!" );
}
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+ $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+ $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
}
- $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw );
+ $dbw->insert( 'revision', $row, __METHOD__ );
- $this->mRecord = $rec;
+ if ( $this->mId === null ) {
+ // Only if auto-increment was used
+ $this->mId = $dbw->insertId();
+ }
+ $commentCallback( $this->mId );
- // TODO: hard-deprecate in 1.32 (or even 1.31?)
- Hooks::run( 'RevisionInsertComplete', [ $this, null, null ] );
+ // Assertion to try to catch T92046
+ if ( (int)$this->mId === 0 ) {
+ throw new UnexpectedValueException(
+ 'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
+ var_export( $row, 1 )
+ );
+ }
- return $rec->getId();
+ // Insert IP revision into ip_changes for use when querying for a range.
+ if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
+ $ipcRow = [
+ 'ipc_rev_id' => $this->mId,
+ 'ipc_rev_timestamp' => $row['rev_timestamp'],
+ 'ipc_hex' => IP::toHex( $row['rev_user_text'] ),
+ ];
+ $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
+ }
+
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $revision = $this;
+ Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
+
+ return $this->mId;
+ }
+
+ protected function checkContentModel() {
+ global $wgContentHandlerUseDB;
+
+ // Note: may return null for revisions that have not yet been inserted
+ $title = $this->getTitle();
+
+ $model = $this->getContentModel();
+ $format = $this->getContentFormat();
+ $handler = $this->getContentHandler();
+
+ if ( !$handler->isSupportedFormat( $format ) ) {
+ $t = $title->getPrefixedDBkey();
+
+ throw new MWException( "Can't use format $format with content model $model on $t" );
+ }
+
+ if ( !$wgContentHandlerUseDB && $title ) {
+ // if $wgContentHandlerUseDB is not set,
+ // all revisions must use the default content model and format.
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultHandler = ContentHandler::getForModelID( $defaultModel );
+ $defaultFormat = $defaultHandler->getDefaultFormat();
+
+ if ( $this->getContentModel() != $defaultModel ) {
+ $t = $title->getPrefixedDBkey();
+
+ throw new MWException( "Can't save non-default content model with "
+ . "\$wgContentHandlerUseDB disabled: model is $model, "
+ . "default for $t is $defaultModel" );
+ }
+
+ if ( $this->getContentFormat() != $defaultFormat ) {
+ $t = $title->getPrefixedDBkey();
+
+ throw new MWException( "Can't use non-default content format with "
+ . "\$wgContentHandlerUseDB disabled: format is $format, "
+ . "default for $t is $defaultFormat" );
+ }
+ }
+
+ $content = $this->getContent( self::RAW );
+ $prefixedDBkey = $title->getPrefixedDBkey();
+ $revId = $this->mId;
+
+ if ( !$content ) {
+ throw new MWException(
+ "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
+ );
+ }
+ if ( !$content->isValid() ) {
+ throw new MWException(
+ "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
+ );
+ }
}
/**
* @return string
*/
public static function base36Sha1( $text ) {
- return SlotRecord::base36Sha1( $text );
+ return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
+ }
+
+ /**
+ * Get the text cache TTL
+ *
+ * @param WANObjectCache $cache
+ * @return int
+ */
+ private static function getCacheTTL( WANObjectCache $cache ) {
+ global $wgRevisionCacheExpiry;
+
+ if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
+ // Do not cache RDBMs blobs in...the RDBMs store
+ $ttl = $cache::TTL_UNCACHEABLE;
+ } else {
+ $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
+ }
+
+ return $ttl;
+ }
+
+ /**
+ * Lazy-load the revision's text.
+ * Currently hardcoded to the 'text' table storage engine.
+ *
+ * @return string|bool The revision's text, or false on failure
+ */
+ private function loadText() {
+ $cache = ObjectCache::getMainWANInstance();
+
+ // No negative caching; negative hits on text rows may be due to corrupted replica DBs
+ return $cache->getWithSetCallback(
+ $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
+ self::getCacheTTL( $cache ),
+ function () {
+ return $this->fetchText();
+ },
+ [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
+ );
+ }
+
+ private function fetchText() {
+ $textId = $this->getTextId();
+
+ // If we kept data for lazy extraction, use it now...
+ if ( $this->mTextRow !== null ) {
+ $row = $this->mTextRow;
+ $this->mTextRow = null;
+ } else {
+ $row = null;
+ }
+
+ // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
+ // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
+ $flags = $this->mQueryFlags;
+ $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
+ ? self::READ_LATEST_IMMUTABLE
+ : 0;
+
+ list( $index, $options, $fallbackIndex, $fallbackOptions ) =
+ DBAccessObjectUtils::getDBOptions( $flags );
+
+ if ( !$row ) {
+ // Text data is immutable; check replica DBs first.
+ $row = wfGetDB( $index )->selectRow(
+ 'text',
+ [ 'old_text', 'old_flags' ],
+ [ 'old_id' => $textId ],
+ __METHOD__,
+ $options
+ );
+ }
+
+ // Fallback to DB_MASTER in some cases if the row was not found
+ if ( !$row && $fallbackIndex !== null ) {
+ // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
+ // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
+ $row = wfGetDB( $fallbackIndex )->selectRow(
+ 'text',
+ [ 'old_text', 'old_flags' ],
+ [ 'old_id' => $textId ],
+ __METHOD__,
+ $fallbackOptions
+ );
+ }
+
+ if ( !$row ) {
+ wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
+ }
+
+ $text = self::getRevisionText( $row );
+ if ( $row && $text === false ) {
+ wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
+ }
+
+ return is_string( $text ) ? $text : false;
}
/**
* @return Revision|null Revision or null on error
*/
public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
- global $wgUser;
- if ( !$user ) {
- $user = $wgUser;
+ global $wgContentHandlerUseDB;
+
+ $fields = [ 'page_latest', 'page_namespace', 'page_title',
+ 'rev_text_id', 'rev_len', 'rev_sha1' ];
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'rev_content_model';
+ $fields[] = 'rev_content_format';
}
- $comment = CommentStoreComment::newUnsavedComment( $summary, null );
+ $current = $dbw->selectRow(
+ [ 'page', 'revision' ],
+ $fields,
+ [
+ 'page_id' => $pageId,
+ 'page_latest=rev_id',
+ ],
+ __METHOD__,
+ [ 'FOR UPDATE' ] // T51581
+ );
+
+ if ( $current ) {
+ if ( !$user ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+
+ $row = [
+ 'page' => $pageId,
+ 'user_text' => $user->getName(),
+ 'user' => $user->getId(),
+ 'comment' => $summary,
+ 'minor_edit' => $minor,
+ 'text_id' => $current->rev_text_id,
+ 'parent_id' => $current->page_latest,
+ 'len' => $current->rev_len,
+ 'sha1' => $current->rev_sha1
+ ];
+
+ if ( $wgContentHandlerUseDB ) {
+ $row['content_model'] = $current->rev_content_model;
+ $row['content_format'] = $current->rev_content_format;
+ }
+
+ $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
- $title = Title::newFromID( $pageId );
- $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user );
+ $revision = new Revision( $row );
+ } else {
+ $revision = null;
+ }
- return new Revision( $rec );
+ return $revision;
}
/**
public static function userCanBitfield( $bitfield, $field, User $user = null,
Title $title = null
) {
- global $wgUser;
-
- if ( !$user ) {
- $user = $wgUser;
+ if ( $bitfield & $field ) { // aspect is deleted
+ if ( $user === null ) {
+ global $wgUser;
+ $user = $wgUser;
+ }
+ 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 call_user_func_array( [ $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;
}
-
- return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title );
}
/**
* @return string|bool False if not found
*/
static function getTimestampFromId( $title, $id, $flags = 0 ) {
- return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags );
+ $db = ( $flags & self::READ_LATEST )
+ ? wfGetDB( DB_MASTER )
+ : wfGetDB( DB_REPLICA );
+ // Casting fix for databases that can't take '' for rev_id
+ if ( $id == '' ) {
+ $id = 0;
+ }
+ $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;
}
/**
* @return int
*/
static function countByPageId( $db, $id ) {
- return self::getRevisionStore()->countRevisionsByPageId( $db, $id );
+ $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
+ [ 'rev_page' => $id ], __METHOD__ );
+ if ( $row ) {
+ return $row->revCount;
+ }
+ return 0;
}
/**
* @return int
*/
static function countByTitle( $db, $title ) {
- return self::getRevisionStore()->countRevisionsByTitle( $db, $title );
+ $id = $title->getArticleID();
+ if ( $id ) {
+ return self::countByPageId( $db, $id );
+ }
+ return 0;
}
/**
* @return bool True if the given user was the only one to edit since the given timestamp
*/
public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
+ if ( !$userId ) {
+ return false;
+ }
+
if ( is_int( $db ) ) {
$db = wfGetDB( $db );
}
- return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since );
+ $res = $db->select( 'revision',
+ 'rev_user',
+ [
+ 'rev_page' => $pageId,
+ 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
+ foreach ( $res as $row ) {
+ if ( $row->rev_user != $userId ) {
+ return false;
+ }
+ }
+ return true;
}
/**
*
* 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.
- * The title will also be loaded if $pageIdOrTitle is an integer ID.
+ * The title will also be lazy loaded, though setTitle() can be used to preload it.
*
- * @param IDatabase $db ignored!
- * @param int|Title $pageIdOrTitle Page ID or Title object
- * @param int $revId Known current revision of this page. Determined automatically if not given.
+ * @param IDatabase $db
+ * @param int $pageId Page ID
+ * @param int $revId Known current revision of this page
* @return Revision|bool Returns false if missing
* @since 1.28
*/
- public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) {
- $title = $pageIdOrTitle instanceof Title
- ? $pageIdOrTitle
- : Title::newFromID( $pageIdOrTitle );
+ public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
+ $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ return $cache->getWithSetCallback(
+ // Page/rev IDs passed in from DB to reflect history merges
+ $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ),
+ $cache::TTL_WEEK,
+ function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
+ $setOpts += Database::getCacheSetOptions( $db );
+
+ $rev = Revision::loadFromPageId( $db, $pageId, $revId );
+ // Reflect revision deletion and user renames
+ if ( $rev ) {
+ $rev->mTitle = null; // mutable; lazy-load
+ $rev->mRefreshMutableFields = true;
+ }
+
+ return $rev ?: false; // don't cache negatives
+ }
+ );
+ }
+
+ /**
+ * For cached revisions, make sure the user name and rev_deleted is up-to-date
+ */
+ private function loadMutableFields() {
+ if ( !$this->mRefreshMutableFields ) {
+ return; // not needed
+ }
- $record = self::getRevisionStore()->getKnownCurrentRevision( $title, $revId );
- return $record ? new Revision( $record ) : false;
+ $this->mRefreshMutableFields = false;
+ $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
+ $row = $dbr->selectRow(
+ [ 'revision', 'user' ],
+ [ 'rev_deleted', 'user_name' ],
+ [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
+ __METHOD__
+ );
+ if ( $row ) { // update values
+ $this->mDeleted = (int)$row->rev_deleted;
+ $this->mUserText = $row->user_name;
+ }
}
}
<?php
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SqlBlobStore;
-use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\TestingAccessWrapper;
/**
* Test cases in RevisionTest should not interact with the Database.
/**
* @dataProvider provideConstructFromArray
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers Revision::constructFromRowArray
*/
- public function testConstructFromArray( $rowArray ) {
- $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+ public function testConstructFromArray( array $rowArray ) {
+ $rev = new Revision( $rowArray );
$this->assertNotNull( $rev->getContent(), 'no content object available' );
$this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
$this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
/**
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers Revision::constructFromRowArray
*/
public function testConstructFromEmptyArray() {
$rev = new Revision( [], 0, $this->getMockTitle() );
99,
'SomeTextUserName',
];
- yield 'user text only' => [
+ // Note: the below XXX test cases are odd and probably result in unexpected behaviour if used
+ // in production code.
+ yield 'XXX: user text only' => [
[
'content' => new JavaScriptContent( 'hello world.' ),
'user_text' => '111.111.111.111',
],
- 0,
+ null,
'111.111.111.111',
];
+ yield 'XXX: user id only' => [
+ [
+ 'content' => new JavaScriptContent( 'hello world.' ),
+ 'user' => 9989,
+ ],
+ 9989,
+ null,
+ ];
}
/**
* @dataProvider provideConstructFromArray_userSetAsExpected
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers Revision::constructFromRowArray
*
* @param array $rowArray
* @param mixed $expectedUserId null to expect the current wgUser ID
$expectedUserName = $testUser->getName();
}
- $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+ $rev = new Revision( $rowArray );
$this->assertEquals( $expectedUserId, $rev->getUser() );
$this->assertEquals( $expectedUserName, $rev->getUserText() );
}
[
'content' => new WikitextContent( 'GOAT' ),
'text_id' => 'someid',
- ],
+ ],
new MWException( "Text already stored in external store (id someid), " .
"can't serialize content object" )
];
- yield 'unknown user id and no user name' => [
- [
- 'content' => new JavaScriptContent( 'hello world.' ),
- 'user' => 9989,
- ],
- new MWException( 'user_text not given, and unknown user ID 9989' )
- ];
yield 'with bad content object (class)' => [
[ 'content' => new stdClass() ],
- new MWException( 'content field must contain a Content object.' )
+ new MWException( '`content` field must contain a Content object.' )
];
yield 'with bad content object (string)' => [
[ 'content' => 'ImAGoat' ],
- new MWException( 'content field must contain a Content object.' )
+ new MWException( '`content` field must contain a Content object.' )
];
yield 'bad row format' => [
'imastring, not a row',
- new InvalidArgumentException(
- '$row must be a row object, an associative array, or a RevisionRecord'
- )
+ new MWException( 'Revision constructor passed invalid row format.' )
];
}
/**
* @dataProvider provideConstructFromArrayThrowsExceptions
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers Revision::constructFromRowArray
*/
public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
$this->setExpectedException(
$expectedException->getMessage(),
$expectedException->getCode()
);
- new Revision( $rowArray, 0, $this->getMockTitle() );
+ new Revision( $rowArray );
}
/**
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
+ * @covers Revision::constructFromRowArray
*/
public function testConstructFromNothing() {
- $this->setExpectedException(
- InvalidArgumentException::class
- );
- new Revision( [] );
+ $rev = new Revision( [] );
+ $this->assertNull( $rev->getId(), 'getId()' );
}
public function provideConstructFromRow() {
yield 'Full construction' => [
[
- 'rev_id' => '42',
- 'rev_page' => '23',
+ 'rev_id' => '2',
+ 'rev_page' => '1',
'rev_text_id' => '2',
'rev_timestamp' => '20171017114835',
'rev_user_text' => '127.0.0.1',
'rev_content_model' => 'GOATMODEL',
],
function ( RevisionTest $testCase, Revision $rev ) {
- $testCase->assertSame( 42, $rev->getId() );
- $testCase->assertSame( 23, $rev->getPage() );
+ $testCase->assertSame( 2, $rev->getId() );
+ $testCase->assertSame( 1, $rev->getPage() );
$testCase->assertSame( 2, $rev->getTextId() );
$testCase->assertSame( '20171017114835', $rev->getTimestamp() );
$testCase->assertSame( '127.0.0.1', $rev->getUserText() );
$testCase->assertSame( 'GOATMODEL', $rev->getContentModel() );
}
];
- yield 'default field values' => [
+ yield 'null fields' => [
[
- 'rev_id' => '42',
- 'rev_page' => '23',
+ 'rev_id' => '2',
+ 'rev_page' => '1',
'rev_text_id' => '2',
'rev_timestamp' => '20171017114835',
'rev_user_text' => '127.0.0.1',
'rev_comment_cid' => null,
],
function ( RevisionTest $testCase, Revision $rev ) {
- // parent ID may be null
- $testCase->assertSame( null, $rev->getParentId(), 'revision id' );
-
- // given fields
- $testCase->assertSame( $rev->getTimestamp(), '20171017114835', 'timestamp' );
- $testCase->assertSame( $rev->getUserText(), '127.0.0.1', 'user name' );
- $testCase->assertSame( $rev->getUser(), 0, 'user id' );
- $testCase->assertSame( $rev->getComment(), 'Goat Comment!' );
- $testCase->assertSame( false, $rev->isMinor(), 'minor edit' );
- $testCase->assertSame( 0, $rev->getVisibility(), 'visibility flags' );
-
- // computed fields
- $testCase->assertNotNull( $rev->getSize(), 'size' );
- $testCase->assertNotNull( $rev->getSha1(), 'hash' );
-
- // NOTE: model and format will be detected based on the namespace of the (mock) title
- $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat(), 'format' );
- $testCase->assertSame( 'wikitext', $rev->getContentModel(), 'model' );
+ $testCase->assertNull( $rev->getSize() );
+ $testCase->assertNull( $rev->getParentId() );
+ $testCase->assertNull( $rev->getSha1() );
+ $testCase->assertSame( 'text/x-wiki', $rev->getContentFormat() );
+ $testCase->assertSame( 'wikitext', $rev->getContentModel() );
}
];
}
/**
* @dataProvider provideConstructFromRow
* @covers Revision::__construct
- * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
+ * @covers Revision::constructFromRowArray
*/
public function testConstructFromRow( array $arrayData, $assertions ) {
- $data = 'Hello goat.'; // needs to match model and format
-
- $blobStore = $this->getMockBuilder( SqlBlobStore::class )
- ->disableOriginalConstructor()
- ->getMock();
-
- $blobStore->method( 'getBlob' )
- ->will( $this->returnValue( $data ) );
-
- $blobStore->method( 'getTextIdFromAddress' )
- ->will( $this->returnCallback(
- function ( $address ) {
- // Turn "tt:1234" into 12345.
- // Note that this must be functional so we can test getTextId().
- // Ideally, we'd un-mock getTextIdFromAddress and use its actual implementation.
- $parts = explode( ':', $address );
- return (int)array_pop( $parts );
- }
- ) );
-
- // Note override internal service, so RevisionStore uses it as well.
- $this->setService( '_SqlBlobStore', $blobStore );
-
$row = (object)$arrayData;
- $rev = new Revision( $row, 0, $this->getMockTitle() );
+ $rev = new Revision( $row );
$assertions( $this, $rev );
}
* @covers Revision::getId
*/
public function testGetId( $rowArray, $expectedId ) {
- $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+ $rev = new Revision( $rowArray );
$this->assertEquals( $expectedId, $rev->getId() );
}
* @covers Revision::setId
*/
public function testSetId( $input, $expected ) {
- $rev = new Revision( [], 0, $this->getMockTitle() );
+ $rev = new Revision( [] );
$rev->setId( $input );
$this->assertSame( $expected, $rev->getId() );
}
* @covers Revision::setUserIdAndName
*/
public function testSetUserIdAndName( $inputId, $expectedId, $name ) {
- $rev = new Revision( [], 0, $this->getMockTitle() );
+ $rev = new Revision( [] );
$rev->setUserIdAndName( $inputId, $name );
$this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) );
$this->assertEquals( $name, $rev->getUserText( Revision::RAW ) );
* @covers Revision::getTextId()
*/
public function testGetTextId( $rowArray, $expected ) {
- $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+ $rev = new Revision( $rowArray );
$this->assertSame( $expected, $rev->getTextId() );
}
* @covers Revision::getParentId()
*/
public function testGetParentId( $rowArray, $expected ) {
- $rev = new Revision( $rowArray, 0, $this->getMockTitle() );
+ $rev = new Revision( $rowArray );
$this->assertSame( $expected, $rev->getParentId() );
}
$this->testGetRevisionText( $expected, $rowData );
}
- private function getWANObjectCache() {
- return new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
- }
-
- /**
- * @return SqlBlobStore
- */
- private function getBlobStore() {
- /** @var LoadBalancer $lb */
- $lb = $this->getMockBuilder( LoadBalancer::class )
- ->disableOriginalConstructor()
- ->getMock();
-
- $cache = $this->getWANObjectCache();
-
- $blobStore = new SqlBlobStore( $lb, $cache );
- return $blobStore;
- }
-
- /**
- * @return RevisionStore
- */
- private function getRevisionStore() {
- /** @var LoadBalancer $lb */
- $lb = $this->getMockBuilder( LoadBalancer::class )
- ->disableOriginalConstructor()
- ->getMock();
-
- $cache = $this->getWANObjectCache();
-
- $blobStore = new RevisionStore( $lb, $this->getBlobStore(), $cache );
- return $blobStore;
- }
-
public function provideGetRevisionTextWithLegacyEncoding() {
yield 'Utf8Native' => [
"Wiki est l'\xc3\xa9cole superieur !",
- 'fr',
'iso-8859-1',
[
'old_flags' => 'utf-8',
];
yield 'Utf8Legacy' => [
"Wiki est l'\xc3\xa9cole superieur !",
- 'fr',
'iso-8859-1',
[
'old_flags' => '',
* @covers Revision::getRevisionText
* @dataProvider provideGetRevisionTextWithLegacyEncoding
*/
- public function testGetRevisionWithLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
- $blobStore = $this->getBlobStore();
- $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
- $this->setService( 'BlobStore', $blobStore );
-
+ public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) {
+ $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
$this->testGetRevisionText( $expected, $rowData );
}
*/
yield 'Utf8NativeGzip' => [
"Wiki est l'\xc3\xa9cole superieur !",
- 'fr',
'iso-8859-1',
[
'old_flags' => 'gzip,utf-8',
];
yield 'Utf8LegacyGzip' => [
"Wiki est l'\xc3\xa9cole superieur !",
- 'fr',
'iso-8859-1',
[
'old_flags' => 'gzip',
* @covers Revision::getRevisionText
* @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding
*/
- public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
+ public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) {
$this->checkPHPExtension( 'zlib' );
-
- $blobStore = $this->getBlobStore();
- $blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
- $this->setService( 'BlobStore', $blobStore );
-
+ $this->setMwGlobals( 'wgLegacyEncoding', $encoding );
$this->testGetRevisionText( $expected, $rowData );
}
*/
public function testCompressRevisionTextUtf8Gzip() {
$this->checkPHPExtension( 'zlib' );
-
- $blobStore = $this->getBlobStore();
- $blobStore->setCompressBlobs( true );
- $this->setService( 'BlobStore', $blobStore );
+ $this->setMwGlobals( 'wgCompressRevisions', true );
$row = new stdClass;
$row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
Revision::getRevisionText( $row ), "getRevisionText" );
}
+ public function provideFetchFromConds() {
+ yield [ 0, [] ];
+ yield [ Revision::READ_LOCKING, [ 'FOR UPDATE' ] ];
+ }
+
/**
- * @covers Revision::loadFromTitle
+ * @dataProvider provideFetchFromConds
+ * @covers Revision::fetchFromConds
*/
- public function testLoadFromTitle() {
- $title = $this->getMockTitle();
-
- $conditions = [
- 'rev_id=page_latest',
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ];
-
- $row = (object)[
- 'rev_id' => '42',
- 'rev_page' => $title->getArticleID(),
- 'rev_text_id' => '2',
- 'rev_timestamp' => '20171017114835',
- 'rev_user_text' => '127.0.0.1',
- 'rev_user' => '0',
- 'rev_minor_edit' => '0',
- 'rev_deleted' => '0',
- 'rev_len' => '46',
- 'rev_parent_id' => '1',
- 'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
- 'rev_comment_text' => 'Goat Comment!',
- 'rev_comment_data' => null,
- 'rev_comment_cid' => null,
- 'rev_content_format' => 'GOATFORMAT',
- 'rev_content_model' => 'GOATMODEL',
- ];
+ public function testFetchFromConds( $flags, array $options ) {
+ $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+ $conditions = [ 'conditionsArray' ];
$db = $this->getMock( IDatabase::class );
- $db->expects( $this->any() )
- ->method( 'getDomainId' )
- ->will( $this->returnValue( wfWikiID() ) );
$db->expects( $this->once() )
->method( 'selectRow' )
->with(
$this->isType( 'array' ),
$this->equalTo( $conditions ),
// Method name
- $this->stringContains( 'fetchRevisionRowFromConds' ),
- // We don't really care about the options here
- $this->isType( 'array' ),
+ $this->equalTo( 'Revision::fetchFromConds' ),
+ $this->equalTo( $options ),
// We don't really care about the join conds are they come from the joinCond methods
$this->isType( 'array' )
)
- ->willReturn( $row );
-
- $revision = Revision::loadFromTitle( $db, $title );
-
- $this->assertEquals( $title->getArticleID(), $revision->getTitle()->getArticleID() );
- $this->assertEquals( $row->rev_id, $revision->getId() );
- $this->assertEquals( $row->rev_len, $revision->getSize() );
- $this->assertEquals( $row->rev_sha1, $revision->getSha1() );
- $this->assertEquals( $row->rev_parent_id, $revision->getParentId() );
- $this->assertEquals( $row->rev_timestamp, $revision->getTimestamp() );
- $this->assertEquals( $row->rev_comment_text, $revision->getComment() );
- $this->assertEquals( $row->rev_user_text, $revision->getUserText() );
+ ->willReturn( 'RETURNVALUE' );
+
+ $wrapper = TestingAccessWrapper::newFromClass( Revision::class );
+ $result = $wrapper->fetchFromConds( $db, $conditions, $flags );
+
+ $this->assertEquals( 'RETURNVALUE', $result );
}
public function provideDecompressRevisionText() {
* @param mixed $expected
*/
public function testDecompressRevisionText( $legacyEncoding, $text, $flags, $expected ) {
- $blobStore = $this->getBlobStore();
- if ( $legacyEncoding ) {
- $blobStore->setLegacyEncoding( $legacyEncoding, Language::factory( 'en' ) );
- }
-
- $this->setService( 'BlobStore', $blobStore );
+ $this->setMwGlobals( 'wgLegacyEncoding', $legacyEncoding );
+ $this->setMwGlobals( 'wgLanguageCode', 'en' );
$this->assertSame(
$expected,
Revision::decompressRevisionText( $text, $flags )
* @covers Revision::getRevisionText
*/
public function testGetRevisionText_external_oldId() {
- $cache = $this->getWANObjectCache();
+ $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$this->setService( 'MainWANObjectCache', $cache );
-
$this->setService(
'ExternalStoreFactory',
new ExternalStoreFactory( [ 'ForTesting' ] )
);
- $lb = $this->getMockBuilder( LoadBalancer::class )
- ->disableOriginalConstructor()
- ->getMock();
-
- $blobStore = new SqlBlobStore( $lb, $cache );
- $this->setService( 'BlobStore', $blobStore );
+ $cacheKey = $cache->makeKey( 'revisiontext', 'textid', '7777' );
$this->assertSame(
'AAAABBAAA',
]
)
);
-
- $cacheKey = $cache->makeKey( 'revisiontext', 'textid', 'tt:7777' );
$this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) );
}
'fields' => [
'ar_id',
'ar_page_id',
- 'ar_namespace',
- 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
- 'ar_namespace',
- 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
- 'ar_namespace',
- 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
- 'ar_namespace',
- 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'fields' => [
'ar_id',
'ar_page_id',
- 'ar_namespace',
- 'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
*/
public function testGetArchiveQueryInfo( $globals, $expected ) {
$this->setMwGlobals( $globals );
-
- $revisionStore = $this->getRevisionStore();
- $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
- $this->setService( 'RevisionStore', $revisionStore );
-
$this->assertEquals(
$expected,
Revision::getArchiveQueryInfo()
*/
public function testGetQueryInfo( $globals, $options, $expected ) {
$this->setMwGlobals( $globals );
-
- $revisionStore = $this->getRevisionStore();
- $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
- $this->setService( 'RevisionStore', $revisionStore );
-
$this->assertEquals(
$expected,
Revision::getQueryInfo( $options )