X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2FRevision.php;h=c7c708ea63ef59c4af81fe9666b681b46d817693;hp=dd3ee782fd24d2b0f4783dea24fe37b8ff7a6ef8;hb=9ae9c681c53dfe030f869e54efe7c2e7e23287b2;hpb=34581a51c0874756861f00084317a4eb7b6152d4 diff --git a/includes/Revision.php b/includes/Revision.php index dd3ee782fd..c7c708ea63 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -20,7 +20,14 @@ * @file */ -use Wikimedia\Rdbms\Database; +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\IDatabase; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; @@ -28,78 +35,50 @@ use Wikimedia\Rdbms\ResultWrapper; use Wikimedia\Rdbms\FakeResultWrapper; /** - * @todo document + * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead. */ class Revision implements IDBAccessObject { - /** @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; + + /** @var RevisionRecord */ + protected $mRecord; // Revision deletion constants - 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 + 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; // Audience options for accessors - const FOR_PUBLIC = 1; - const FOR_THIS_USER = 2; - const RAW = 3; + 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; + + /** + * @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 ) + ); + } - const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count + return $store; + } /** * Load a page revision from a given revision ID number. @@ -114,7 +93,8 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public static function newFromId( $id, $flags = 0 ) { - return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags ); + $rec = self::getRevisionStore()->getRevisionById( $id, $flags ); + return $rec === null ? null : new Revision( $rec, $flags ); } /** @@ -132,20 +112,8 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) { - $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 ); - } + $rec = self::getRevisionStore()->getRevisionByTitle( $linkTarget, $id, $flags ); + return $rec === null ? null : new Revision( $rec, $flags ); } /** @@ -163,22 +131,13 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) { - $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 ); - } + $rec = self::getRevisionStore()->getRevisionByPageId( $pageId, $revId, $flags ); + return $rec === null ? null : new Revision( $rec, $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 @@ -187,68 +146,45 @@ class Revision implements IDBAccessObject { * @return Revision */ public static function newFromArchiveRow( $row, $overrides = [] ) { - 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 probably came 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 ); + $rec = self::getRevisionStore()->newRevisionFromArchiveRow( $row, 0, null, $overrides ); + return new Revision( $rec ); } /** * @since 1.19 * - * @param object $row + * 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 * @return Revision */ public static function newFromRow( $row ) { - return new self( $row ); + if ( is_array( $row ) ) { + $rec = self::getRevisionStore()->newMutableRevisionFromArray( $row ); + } else { + $rec = self::getRevisionStore()->newRevisionFromRow( $row ); + } + + return new Revision( $rec ); } /** * 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 ) { - return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] ); + wfDeprecated( __METHOD__, '1.31' ); // no known callers + $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id ); + return $rec === null ? null : new Revision( $rec ); } /** @@ -256,19 +192,16 @@ class Revision implements IDBAccessObject { * 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 ) { - $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 ); + $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id ); + return $rec === null ? null : new Revision( $rec ); } /** @@ -276,24 +209,16 @@ class Revision implements IDBAccessObject { * 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 ) { - if ( $id ) { - $matchId = intval( $id ); - } else { - $matchId = 'page_latest'; - } - return self::loadFromConds( $db, - [ - "rev_id=$matchId", - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() - ] - ); + $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id ); + return $rec === null ? null : new Revision( $rec ); } /** @@ -301,73 +226,17 @@ class Revision implements IDBAccessObject { * 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 ) { - 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; + // XXX: replace loadRevisionFromTimestamp by getRevisionByTimestamp? + $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp ); + return $rec === null ? null : new Revision( $rec ); } /** @@ -377,58 +246,22 @@ class Revision implements IDBAccessObject { * * @param LinkTarget $title * @return ResultWrapper - * @deprecated Since 1.28 + * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31. */ public static function fetchRevision( LinkTarget $title ) { - $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 ) { - $fields = array_merge( - self::selectFields(), - self::selectPageFields(), - self::selectUserFields() - ); - $options = []; - if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) { - $options[] = 'FOR UPDATE'; - } - return $db->selectRow( - [ 'revision', 'page', 'user' ], - $fields, - $conditions, - __METHOD__, - $options, - [ 'page' => self::pageJoinCond(), 'user' => self::userJoinCond() ] - ); + wfDeprecated( __METHOD__, '1.31' ); + return new FakeResultWrapper( [] ); } /** * 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. * @return array */ public static function userJoinCond() { + wfDeprecated( __METHOD__, '1.31' ); return [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ]; } @@ -436,22 +269,25 @@ class Revision implements IDBAccessObject { * 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. * @return array */ public static function pageJoinCond() { + wfDeprecated( __METHOD__, '1.31' ); return [ 'INNER JOIN', [ 'page_id = rev_page' ] ]; } /** * Return the list of revision fields that should be selected to create * a new revision. - * @todo Deprecate this in favor of a method that returns tables and joins - * as well, and use CommentStore::getJoin(). + * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead. * @return array */ public static function selectFields() { global $wgContentHandlerUseDB; + wfDeprecated( __METHOD__, '1.31' ); + $fields = [ 'rev_id', 'rev_page', @@ -479,12 +315,14 @@ class Revision implements IDBAccessObject { /** * Return the list of revision fields that should be selected to create * a new revision from an archive row. - * @todo Deprecate this in favor of a method that returns tables and joins - * as well, and use CommentStore::getJoin(). + * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead. * @return array */ public static function selectArchiveFields() { global $wgContentHandlerUseDB; + + wfDeprecated( __METHOD__, '1.31' ); + $fields = [ 'ar_id', 'ar_page_id', @@ -513,9 +351,11 @@ class Revision implements IDBAccessObject { /** * Return the list of text fields that should be selected to read the * revision text + * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead. * @return array */ public static function selectTextFields() { + wfDeprecated( __METHOD__, '1.31' ); return [ 'old_text', 'old_flags' @@ -524,9 +364,11 @@ class Revision implements IDBAccessObject { /** * Return the list of page fields that should be selected from page table + * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead. * @return array */ public static function selectPageFields() { + wfDeprecated( __METHOD__, '1.31' ); return [ 'page_namespace', 'page_title', @@ -539,216 +381,96 @@ class Revision implements IDBAccessObject { /** * Return the list of user fields that should be selected from user table + * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead. * @return array */ public static function selectUserFields() { + wfDeprecated( __METHOD__, '1.31' ); return [ 'user_name' ]; } /** - * Do a batched query to get the parent revision lengths - * @param IDatabase $db - * @param array $revIds - * @return array + * 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 + * - 'text': Join with the text table, and select fields to load page text + * @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 getParentLengths( $db, array $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; + public static function getQueryInfo( $options = [] ) { + return self::getRevisionStore()->getQueryInfo( $options ); } /** - * @param object|array $row Either a database row or an array - * @throws MWException - * @access private + * 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 function __construct( $row ) { - if ( is_object( $row ) ) { - $this->constructFromDbRowObject( $row ); - } elseif ( is_array( $row ) ) { - $this->constructFromRowArray( $row ); - } else { - throw new MWException( 'Revision constructor passed invalid row format.' ); - } - $this->mUnpatrolled = null; + public static function getArchiveQueryInfo() { + return self::getRevisionStore()->getArchiveQueryInfo(); } /** - * @param object $row + * Do a batched query to get the parent revision lengths + * @param IDatabase $db + * @param array $revIds + * @return array */ - 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 probably came 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 { - $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; + public static function getParentLengths( $db, array $revIds ) { + return self::getRevisionStore()->listRevisionSizes( $db, $revIds ); } /** - * @param array $row + * @param object|array|RevisionRecord $row Either a database row or an array + * @param int $queryFlags + * @param Title|null $title * - * @throws MWException + * @access private */ - private function constructFromRowArray( array $row ) { - // Build a new revision to be saved... - global $wgUser; // ugh + function __construct( $row, $queryFlags = 0, Title $title = null ) { + global $wgUser; - # 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." ); + if ( $row instanceof RevisionRecord ) { + $this->mRecord = $row; + } elseif ( is_array( $row ) ) { + if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) { + $row['user'] = $wgUser; } - } - - $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 ); + $this->mRecord = self::getRevisionStore()->newMutableRevisionFromArray( + $row, + $queryFlags, + $title + ); + } elseif ( is_object( $row ) ) { + $this->mRecord = self::getRevisionStore()->newRevisionFromRow( + $row, + $queryFlags, + $title + ); + } else { + throw new InvalidArgumentException( + '$row must be a row object, an associative array, or a RevisionRecord' + ); } + } - // force lazy init - $this->getContentModel(); - $this->getContentFormat(); + /** + * @return RevisionRecord + */ + public function getRevisionRecord() { + return $this->mRecord; } /** @@ -757,19 +479,27 @@ class Revision implements IDBAccessObject { * @return int|null */ public function getId() { - return $this->mId; + return $this->mRecord->getId(); } /** * Set the revision ID * - * This should only be used for proposed revisions that turn out to be null edits + * 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.19 - * @param int $id + * @param int|string $id + * @throws MWException */ public function setId( $id ) { - $this->mId = (int)$id; + if ( $this->mRecord instanceof MutableRevisionRecord ) { + $this->mRecord->setId( intval( $id ) ); + } else { + throw new MWException( __METHOD__ . ' is not supported on this instance' ); + } } /** @@ -777,95 +507,107 @@ class Revision implements IDBAccessObject { * * 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 ) { - $this->mUser = (int)$id; - $this->mUserText = $name; - $this->mOrigUserText = $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' ); + } } /** - * Get text row ID + * @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. * * @return int|null */ public function getTextId() { - return $this->mTextId; + $slot = $this->getMainSlotRaw(); + return $slot->hasAddress() + ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() ) + : null; } /** * Get parent revision ID (the original previous page revision) * - * @return int|null + * @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. */ public function getParentId() { - return $this->mParentId; + return $this->mRecord->getParentId(); } /** * Returns the length of the text in this revision, or null if unknown. * - * @return int|null + * @return int */ public function getSize() { - return $this->mSize; + return $this->mRecord->getSize(); } /** - * Returns the base36 sha1 of the text in this revision, or null if unknown. + * Returns the base36 sha1 of the content in this revision, or null if unknown. * - * @return string|null + * @return string */ public function getSha1() { - return $this->mSha1; + // XXX: we may want to drop all the hashing logic, it's not worth the overhead. + return $this->mRecord->getSha1(); } /** - * Returns the title of the page associated with this entry or null. + * Returns the title of the page associated with this entry. + * Since 1.31, this will never return null. * * Will do a query, when title is not set and id is given. * - * @return Title|null + * @return Title */ public function getTitle() { - 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 ); - $row = $dbr->selectRow( - [ 'page', 'revision' ], - self::selectPageFields(), - [ 'page_id=rev_page', 'rev_id' => $this->mId ], - __METHOD__ - ); - 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; + $linkTarget = $this->mRecord->getPageAsLinkTarget(); + return Title::newFromLinkTarget( $linkTarget ); } /** * 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 ) { - $this->mTitle = $title; + if ( !$title->equals( $this->getTitle() ) ) { + throw new InvalidArgumentException( + $title->getPrefixedText() + . ' is not the same as ' + . $this->mRecord->getPageAsLinkTarget()->__toString() + ); + } } /** @@ -874,7 +616,7 @@ class Revision implements IDBAccessObject { * @return int|null */ public function getPage() { - return $this->mPage; + return $this->mRecord->getPageId(); } /** @@ -891,13 +633,14 @@ class Revision implements IDBAccessObject { * @return int */ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) { - 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; + global $wgUser; + + if ( $audience === self::FOR_THIS_USER && !$user ) { + $user = $wgUser; } + + $user = $this->mRecord->getUser( $audience, $user ); + return $user ? $user->getId() : 0; } /** @@ -925,23 +668,14 @@ class Revision implements IDBAccessObject { * @return string */ public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) { - $this->loadMutableFields(); + global $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; + if ( $audience === self::FOR_THIS_USER && !$user ) { + $user = $wgUser; } + + $user = $this->mRecord->getUser( $audience, $user ); + return $user ? $user->getName() : ''; } /** @@ -969,13 +703,14 @@ class Revision implements IDBAccessObject { * @return string */ function getComment( $audience = self::FOR_PUBLIC, User $user = null ) { - 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; + global $wgUser; + + if ( $audience === self::FOR_THIS_USER && !$user ) { + $user = $wgUser; } + + $comment = $this->mRecord->getComment( $audience, $user ); + return $comment === null ? null : $comment->text; } /** @@ -993,23 +728,14 @@ class Revision implements IDBAccessObject { * @return bool */ public function isMinor() { - return (bool)$this->mMinorEdit; + return $this->mRecord->isMinor(); } /** * @return int Rcid of the unpatrolled row, zero if there isn't one */ public function isUnpatrolled() { - 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 self::getRevisionStore()->isUnpatrolled( $this->mRecord ); } /** @@ -1022,19 +748,7 @@ class Revision implements IDBAccessObject { * @return RecentChange|null */ public function getRecentChange( $flags = 0 ) { - $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 self::getRevisionStore()->getRecentChange( $this->mRecord, $flags ); } /** @@ -1043,14 +757,7 @@ class Revision implements IDBAccessObject { * @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 ( $this->getVisibility() & $field ) == $field; + return $this->mRecord->isDeleted( $field ); } /** @@ -1059,19 +766,17 @@ class Revision implements IDBAccessObject { * @return int */ public function getVisibility() { - $this->loadMutableFields(); - - return (int)$this->mDeleted; + return $this->mRecord->getVisibility(); } /** * Fetch revision content if it's available to the specified audience. * If the specified audience does not have the ability to view this - * revision, null will be returned. + * revision, or the content could not be loaded, 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 $wgUser + * Revision::FOR_THIS_USER to be displayed to $user * 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 @@ -1079,12 +784,17 @@ class Revision implements IDBAccessObject { * @return Content|null */ public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) { - if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) { - return null; - } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) { + global $wgUser; + + if ( $audience === self::FOR_THIS_USER && !$user ) { + $user = $wgUser; + } + + try { + return $this->mRecord->getContent( 'main', $audience, $user ); + } + catch ( RevisionAccessException $e ) { return null; - } else { - return $this->getContentInternal(); } } @@ -1092,86 +802,51 @@ class Revision implements IDBAccessObject { * Get original serialized data (without checking view restrictions) * * @since 1.21 + * @deprecated since 1.31, use BlobStore::getBlob instead. + * * @return string */ public function getSerializedData() { - if ( $this->mText === null ) { - // Revision is immutable. Load on demand. - $this->mText = $this->loadText(); - } - - return $this->mText; - } - - /** - * 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; + $slot = $this->getMainSlotRaw(); + return $slot->getContent()->serialize(); } /** - * Returns the content model for this revision. + * Returns the content model for the main slot of 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() { - 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; + return $this->getMainSlotRaw()->getModel(); } /** - * Returns the content format for this revision. + * Returns the content format for the main slot of 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() { - if ( !$this->mContentFormat ) { - $handler = $this->getContentHandler(); - $this->mContentFormat = $handler->getDefaultFormat(); + $format = $this->getMainSlotRaw()->getFormat(); - assert( !empty( $this->mContentFormat ) ); + if ( $format === null ) { + // if no format was stored along with the blob, fall back to default format + $format = $this->getContentHandler()->getDefaultFormat(); } - return $this->mContentFormat; + return $format; } /** @@ -1181,33 +856,21 @@ class Revision implements IDBAccessObject { * @return ContentHandler */ public function getContentHandler() { - 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 ContentHandler::getForModelID( $this->getContentModel() ); } /** * @return string */ public function getTimestamp() { - return wfTimestamp( TS_MW, $this->mTimestamp ); + return $this->mRecord->getTimestamp(); } /** * @return bool */ public function isCurrent() { - return $this->mCurrent; + return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent(); } /** @@ -1216,13 +879,8 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public function getPrevious() { - if ( $this->getTitle() ) { - $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() ); - if ( $prev ) { - return self::newFromTitle( $this->getTitle(), $prev ); - } - } - return null; + $rec = self::getRevisionStore()->getPreviousRevision( $this->mRecord ); + return $rec === null ? null : new Revision( $rec ); } /** @@ -1231,38 +889,8 @@ class Revision implements IDBAccessObject { * @return Revision|null */ public function getNext() { - 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 ); + $rec = self::getRevisionStore()->getNextRevision( $this->mRecord ); + return $rec === null ? null : new Revision( $rec ); } /** @@ -1295,35 +923,9 @@ class Revision implements IDBAccessObject { return false; } - // 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 ] ); - } - } + $cacheKey = isset( $row->old_id ) ? ( 'tt:' . $row->old_id ) : null; - return self::decompressRevisionText( $text, $flags ); + return self::getBlobStore()->expandBlob( $text, $flags, $cacheKey ); } /** @@ -1337,28 +939,7 @@ class Revision implements IDBAccessObject { * @return string */ public static function compressRevisionText( &$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 self::getBlobStore()->compressData( $text ); } /** @@ -1369,46 +950,7 @@ class Revision implements IDBAccessObject { * @return string|bool Decompressed text, or false on failure */ public static function decompressRevisionText( $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 self::getBlobStore()->decompressData( $text, $flags ); } /** @@ -1420,192 +962,29 @@ class Revision implements IDBAccessObject { * @return int The revision ID */ public function insertOn( $dbw ) { - 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" ); - } + global $wgUser; - $this->checkContentModel(); + // Note that $this->mRecord->getId() will typically return null here, but not always, + // e.g. not when restoring a revision. - $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!" ); + 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.' ); } - - $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; } - $dbw->insert( 'revision', $row, __METHOD__ ); - - if ( $this->mId === null ) { - // Only if auto-increment was used - $this->mId = $dbw->insertId(); - } - $commentCallback( $this->mId ); - - // 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 ) - ); - } + $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw ); - // 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__ ); - } + $this->mRecord = $rec; // 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(); + // TODO: hard-deprecate in 1.32 (or even 1.31?) + Hooks::run( 'RevisionInsertComplete', [ &$revision, null, null ] ); - 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 $rec->getId(); } /** @@ -1614,103 +993,7 @@ class Revision implements IDBAccessObject { * @return string */ public static function 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 SlotRecord::base36Sha1( $text ); } /** @@ -1729,58 +1012,17 @@ class Revision implements IDBAccessObject { * @return Revision|null Revision or null on error */ public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) { - 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'; + global $wgUser; + if ( !$user ) { + $user = $wgUser; } - $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 ); + $comment = CommentStoreComment::newUnsavedComment( $summary, null ); - $revision = new Revision( $row ); - } else { - $revision = null; - } + $title = Title::newFromID( $pageId ); + $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user ); - return $revision; + return new Revision( $rec ); } /** @@ -1814,35 +1056,13 @@ class Revision implements IDBAccessObject { public static function userCanBitfield( $bitfield, $field, User $user = null, Title $title = null ) { - 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; + global $wgUser; + + if ( !$user ) { + $user = $wgUser; } + + return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title ); } /** @@ -1854,18 +1074,7 @@ class Revision implements IDBAccessObject { * @return string|bool False if not found */ static function getTimestampFromId( $title, $id, $flags = 0 ) { - $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 self::getRevisionStore()->getTimestampFromId( $title, $id, $flags ); } /** @@ -1876,12 +1085,7 @@ class Revision implements IDBAccessObject { * @return int */ static function countByPageId( $db, $id ) { - $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ], - [ 'rev_page' => $id ], __METHOD__ ); - if ( $row ) { - return $row->revCount; - } - return 0; + return self::getRevisionStore()->countRevisionsByPageId( $db, $id ); } /** @@ -1892,11 +1096,7 @@ class Revision implements IDBAccessObject { * @return int */ static function countByTitle( $db, $title ) { - $id = $title->getArticleID(); - if ( $id ) { - return self::countByPageId( $db, $id ); - } - return 0; + return self::getRevisionStore()->countRevisionsByTitle( $db, $title ); } /** @@ -1916,28 +1116,11 @@ class Revision implements IDBAccessObject { * @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 ); } - $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; + return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since ); } /** @@ -1945,54 +1128,20 @@ class Revision implements IDBAccessObject { * * 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 lazy loaded, though setTitle() can be used to preload it. + * The title will also be loaded if $pageIdOrTitle is an integer ID. * - * @param IDatabase $db - * @param int $pageId Page ID - * @param int $revId Known current revision of this page + * @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. * @return Revision|bool Returns false if missing * @since 1.28 */ - 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 - } - ); - } + public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) { + $title = $pageIdOrTitle instanceof Title + ? $pageIdOrTitle + : Title::newFromID( $pageIdOrTitle ); - /** - * For cached revisions, make sure the user name and rev_deleted is up-to-date - */ - private function loadMutableFields() { - if ( !$this->mRefreshMutableFields ) { - return; // not needed - } - - $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; - } + $record = self::getRevisionStore()->getKnownCurrentRevision( $title, $revId ); + return $record ? new Revision( $record ) : false; } }