37use InvalidArgumentException;
51use Psr\Log\LoggerAwareInterface;
52use Psr\Log\LoggerInterface;
53use Psr\Log\NullLogger;
63use Wikimedia\Assert\Assert;
172 Assert::parameterType(
'string|boolean',
$dbDomain,
'$dbDomain' );
176 '$mcrMigrationStage',
177 'Reading from the old and the new schema at the same time is not supported.'
181 '$mcrMigrationStage',
182 'Reading needs to be enabled for the old or the new schema.'
186 '$mcrMigrationStage',
187 'Writing needs to be enabled for the new schema.'
192 '$mcrMigrationStage',
193 'Cannot read the old schema when not also writing it.'
206 $this->logger =
new NullLogger();
215 return ( $this->mcrMigrationStage & $flags ) === $flags;
227 "Cross-wiki content loading is not supported by the pre-MCR schema"
240 return $this->blobStore->isReadOnly();
261 'Content model must be stored in the database for multi content revision migration.'
293 return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
310 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
311 if ( !$pageId && !$revId ) {
312 throw new InvalidArgumentException(
'$pageId and $revId cannot both be 0 or null' );
318 $queryFlags = self::READ_NORMAL;
321 $canUseTitleNewFromId = ( $pageId !==
null && $pageId > 0 && $this->dbDomain === false );
325 if ( $canUseTitleNewFromId ) {
326 $titleFlags = ( $dbMode ==
DB_MASTER ? Title::READ_LATEST : 0 );
328 $title = Title::newFromID( $pageId, $titleFlags );
335 $canUseRevId = ( $revId !==
null && $revId > 0 );
337 if ( $canUseRevId ) {
340 $row =
$dbr->selectRow(
341 [
'revision',
'page' ],
350 [
'rev_id' => $revId ],
353 [
'page' => [
'JOIN',
'page_id=rev_page' ] ]
357 return Title::newFromRow( $row );
366 __METHOD__ .
' fell back to READ_LATEST and got a Title.',
374 "Could not determine title for page ID $pageId and revision ID $revId"
386 if ( $value ===
null ) {
388 "$name must not be " . var_export( $value,
true ) .
"!"
403 if ( $value ===
null || $value === 0 || $value ===
'' ) {
405 "$name must not be " . var_export( $value,
true ) .
"!"
432 throw new InvalidArgumentException(
433 'main slot must be provided'
441 throw new InvalidArgumentException(
442 'Only the main slot is supported when not writing to the MCR enabled schema!'
450 throw new InvalidArgumentException(
451 'Only the main slot is supported when not reading from the MCR enabled schema!'
461 $this->
failOnNull( $user->getId(),
'user field' );
462 $this->
failOnEmpty( $user->getName(),
'user_text field' );
481 function (
IDatabase $dbw, $fname ) use (
502 Assert::postcondition( $rev->
getId() > 0,
'revision must have an ID' );
503 Assert::postcondition( $rev->
getPageId() > 0,
'revision must have a page ID' );
504 Assert::postcondition(
506 'revision must have a comment'
508 Assert::postcondition(
510 'revision must have a user'
519 foreach ( $slotRoles as $role ) {
521 Assert::postcondition(
522 $slot->getContent() !==
null,
523 $role .
' slot must have content'
525 Assert::postcondition(
526 $slot->hasRevision(),
527 $role .
' slot must have a revision associated'
531 Hooks::run(
'RevisionRecordInserted', [ $rev ] );
534 $legacyRevision =
new Revision( $rev );
535 Hooks::run(
'RevisionInsertComplete', [ &$legacyRevision,
null,
null ] );
558 $revisionId = $revisionRow[
'rev_id'];
567 foreach ( $slotRoles as $role ) {
577 if ( $slot->hasRevision() && $slot->hasContentId() ) {
580 $slot->getRevision() === $revisionId,
581 'slot role ' . $slot->getRole(),
582 'Existing slot should belong to revision '
583 . $revisionId .
', but belongs to revision ' . $slot->getRevision() .
'!'
589 $newSlots[$role] = $slot;
595 $blobAddress = $slot->getAddress();
599 $newSlots[$role] = $this->
insertSlotOn( $dbw, $revisionId, $slot,
$title, $blobHints );
609 (
object)$revisionRow,
625 $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
627 throw new LogicException(
628 'Blob address not supported in 1.29 database schema: ' . $blobAddress
634 $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
638 [
'rev_text_id' => $textId ],
639 [
'rev_id' => $revisionId ],
659 array $blobHints = []
712 if ( $user->
getId() === 0 && IP::isValid( $user->
getName() ) ) {
714 'ipc_rev_id' => $revisionId,
716 'ipc_hex' => IP::toHex( $user->
getName() ),
718 $dbw->
insert(
'ip_changes', $ipcRow, __METHOD__ );
741 list( $commentFields, $commentCallback ) =
742 $this->commentStore->insertWithTempTable(
747 $revisionRow += $commentFields;
749 list( $actorFields, $actorCallback ) =
750 $this->actorMigration->getInsertValuesWithTempTable(
755 $revisionRow += $actorFields;
757 $dbw->
insert(
'revision', $revisionRow, __METHOD__ );
759 if ( !isset( $revisionRow[
'rev_id'] ) ) {
761 $revisionRow[
'rev_id'] = intval( $dbw->
insertId() );
763 if ( $dbw->
getType() ===
'mysql' ) {
768 $maxRevId = intval( $dbw->
selectField(
'archive',
'MAX(ar_rev_id)',
'', __METHOD__ ) );
771 $maxRevId2 = intval( $dbw->
selectField(
'slots',
'MAX(slot_revision_id)',
'', __METHOD__ ) );
772 if ( $maxRevId2 >= $maxRevId ) {
773 $maxRevId = $maxRevId2;
778 if ( $maxRevId >= $revisionRow[
'rev_id'] ) {
779 $this->logger->debug(
780 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
781 .
' Trying to fix it.',
783 'revid' => $revisionRow[
'rev_id'],
785 'maxrevid' => $maxRevId,
789 if ( !$dbw->
lock(
'fix-for-T202032', __METHOD__ ) ) {
790 throw new MWException(
'Failed to get database lock for T202032' );
794 function ( $trigger,
IDatabase $dbw ) use ( $fname ) {
795 $dbw->
unlock(
'fix-for-T202032', $fname );
799 $dbw->
delete(
'revision', [
'rev_id' => $revisionRow[
'rev_id'] ], __METHOD__ );
811 $dbw->
selectSQLText(
'archive', [
'v' =>
"MAX(ar_rev_id)" ],
'', __METHOD__ ) .
' FOR UPDATE'
815 $dbw->
selectSQLText(
'slots', [
'v' =>
"MAX(slot_revision_id)" ],
'', __METHOD__ )
823 $row1 ? intval( $row1->v ) : 0,
824 $row2 ? intval( $row2->v ) : 0
830 $revisionRow[
'rev_id'] = $maxRevId + 1;
831 $dbw->
insert(
'revision', $revisionRow, __METHOD__ );
836 $commentCallback( $revisionRow[
'rev_id'] );
837 $actorCallback( $revisionRow[
'rev_id'], $revisionRow );
861 'rev_parent_id' => $parentId,
862 'rev_minor_edit' => $rev->
isMinor() ? 1 : 0,
869 if ( $rev->
getId() !==
null ) {
871 $revisionRow[
'rev_id'] = $rev->
getId();
877 $model = $mainSlot->getModel();
878 $format = $mainSlot->getFormat();
881 if ( $this->contentHandlerUseDB ) {
884 $defaultModel = ContentHandler::getDefaultModelFor(
$title );
885 $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
887 $revisionRow[
'rev_content_model'] = ( $model === $defaultModel ) ?
null : $model;
888 $revisionRow[
'rev_content_format'] = ( $format === $defaultFormat ) ?
null : $format;
906 array $blobHints = []
909 $format =
$content->getDefaultFormat();
914 return $this->blobStore->storeBlob(
939 'slot_revision_id' => $revisionId,
940 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->
getRole() ),
941 'slot_content_id' => $contentId,
946 $dbw->
insert(
'slots', $slotRow, __METHOD__ );
957 'content_size' => $slot->
getSize(),
958 'content_sha1' => $slot->
getSha1(),
959 'content_model' => $this->contentModelStore->acquireId( $slot->
getModel() ),
960 'content_address' => $blobAddress,
962 $dbw->
insert(
'content', $contentRow, __METHOD__ );
980 $format =
$content->getDefaultFormat();
981 $handler =
$content->getContentHandler();
985 if ( !$handler->isSupportedFormat( $format ) ) {
986 throw new MWException(
"Can't use format $format with content model $model on $name" );
989 if ( !$this->contentHandlerUseDB ) {
995 $roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
996 $defaultModel = $roleHandler->getDefaultModel(
$title );
997 $defaultHandler = ContentHandler::getForModelID( $defaultModel );
998 $defaultFormat = $defaultHandler->getDefaultFormat();
1000 if ( $model != $defaultModel ) {
1001 throw new MWException(
"Can't save non-default content model with "
1002 .
"\$wgContentHandlerUseDB disabled: model is $model, "
1003 .
"default for $name is $defaultModel"
1007 if ( $format != $defaultFormat ) {
1008 throw new MWException(
"Can't use non-default content format with "
1009 .
"\$wgContentHandlerUseDB disabled: format is $format, "
1010 .
"default for $name is $defaultFormat"
1017 "New content for $name is not valid! Content model is $model"
1056 $pageId =
$title->getArticleID();
1064 [
'page_id' => $pageId ],
1069 if ( !$pageLatest ) {
1076 [
'rev_id' => intval( $pageLatest ) ],
1081 if ( !$oldRevision ) {
1082 $msg =
"Failed to load latest revision ID $pageLatest of page ID $pageId.";
1083 $this->logger->error(
1085 [
'exception' =>
new RuntimeException( $msg ) ]
1094 $newRevision->setComment( $comment );
1095 $newRevision->setUser( $user );
1096 $newRevision->setTimestamp( $timestamp );
1097 $newRevision->setMinorEdit( $minor );
1099 return $newRevision;
1114 return $rc->getAttribute(
'rc_id' );
1139 if ( !$userIdentity ) {
1146 $actorWhere = $this->actorMigration->getWhere( $db,
'rc_user', $rev->
getUser(),
false );
1149 $actorWhere[
'conds'],
1150 'rc_timestamp' => $db->timestamp( $rev->
getTimestamp() ),
1151 'rc_this_oldid' => $rev->
getId()
1174 'ar_page_id' =>
'rev_page',
1175 'ar_rev_id' =>
'rev_id',
1178 'ar_text_id' =>
'rev_text_id',
1179 'ar_timestamp' =>
'rev_timestamp',
1180 'ar_user_text' =>
'rev_user_text',
1181 'ar_user' =>
'rev_user',
1182 'ar_actor' =>
'rev_actor',
1183 'ar_minor_edit' =>
'rev_minor_edit',
1184 'ar_deleted' =>
'rev_deleted',
1185 'ar_len' =>
'rev_len',
1186 'ar_parent_id' =>
'rev_parent_id',
1187 'ar_sha1' =>
'rev_sha1',
1188 'ar_comment' =>
'rev_comment',
1189 'ar_comment_cid' =>
'rev_comment_cid',
1190 'ar_comment_id' =>
'rev_comment_id',
1191 'ar_comment_text' =>
'rev_comment_text',
1192 'ar_comment_data' =>
'rev_comment_data',
1193 'ar_comment_old' =>
'rev_comment_old',
1194 'ar_content_format' =>
'rev_content_format',
1195 'ar_content_model' =>
'rev_content_model',
1198 $revRow =
new stdClass();
1199 foreach ( $fieldMap as $arKey => $revKey ) {
1200 if ( property_exists( $archiveRow, $arKey ) ) {
1201 $revRow->$revKey = $archiveRow->$arKey;
1219 $mainSlotRow =
new stdClass();
1221 $mainSlotRow->model_name =
null;
1222 $mainSlotRow->slot_revision_id =
null;
1223 $mainSlotRow->slot_content_id =
null;
1224 $mainSlotRow->content_address =
null;
1230 if ( is_object( $row ) ) {
1234 throw new LogicException(
'Can\'t emulate the main slot when using MCR schema.' );
1238 if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
1242 if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
1243 $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
1249 $mainSlotRow->slot_origin = isset( $row->slot_origin )
1250 ? intval( $row->slot_origin )
1253 if ( isset( $row->old_text ) ) {
1255 $blobData = isset( $row->old_text ) ? strval( $row->old_text ) :
null;
1257 if ( !property_exists( $row,
'old_flags' ) ) {
1258 throw new InvalidArgumentException(
'old_flags was not set in $row' );
1260 $blobFlags = $row->old_flags ??
'';
1263 $mainSlotRow->slot_revision_id = intval( $row->rev_id );
1265 $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) :
null;
1266 $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) :
null;
1267 $mainSlotRow->model_name = isset( $row->rev_content_model )
1268 ? strval( $row->rev_content_model )
1271 $mainSlotRow->format_name = isset( $row->rev_content_format )
1272 ? strval( $row->rev_content_format )
1275 if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
1277 $mainSlotRow->slot_content_id
1280 } elseif ( is_array( $row ) ) {
1281 $mainSlotRow->slot_revision_id = isset( $row[
'id'] ) ? intval( $row[
'id'] ) :
null;
1283 $mainSlotRow->slot_origin = isset( $row[
'slot_origin'] )
1284 ? intval( $row[
'slot_origin'] )
1286 $mainSlotRow->content_address = isset( $row[
'text_id'] )
1287 ? SqlBlobStore::makeAddressFromTextId( intval( $row[
'text_id'] ) )
1289 $mainSlotRow->content_size = isset( $row[
'len'] ) ? intval( $row[
'len'] ) :
null;
1290 $mainSlotRow->content_sha1 = isset( $row[
'sha1'] ) ? strval( $row[
'sha1'] ) :
null;
1292 $mainSlotRow->model_name = isset( $row[
'content_model'] )
1293 ? strval( $row[
'content_model'] ) :
null;
1295 $mainSlotRow->format_name = isset( $row[
'content_format'] )
1296 ? strval( $row[
'content_format'] ) :
null;
1297 $blobData = isset( $row[
'text'] ) ? rtrim( strval( $row[
'text'] ) ) :
null;
1300 $blobFlags = isset( $row[
'flags'] ) ? trim( strval( $row[
'flags'] ) ) :
null;
1303 if ( !empty( $row[
'content'] ) ) {
1304 if ( !( $row[
'content'] instanceof
Content ) ) {
1305 throw new MWException(
'content field must contain a Content object.' );
1310 $handler =
$content->getContentHandler();
1312 $mainSlotRow->model_name =
$content->getModel();
1315 if ( $mainSlotRow->format_name ===
null ) {
1316 $mainSlotRow->format_name = $handler->getDefaultFormat();
1320 if ( isset( $row[
'text_id'] ) && intval( $row[
'text_id'] ) > 0 ) {
1322 $mainSlotRow->slot_content_id
1326 throw new MWException(
'Revision constructor passed invalid row format.' );
1331 if ( !isset( $mainSlotRow->slot_origin ) ) {
1332 $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
1335 if ( $mainSlotRow->model_name ===
null ) {
1339 return $this->slotRoleRegistry->getRoleHandler( $slot->getRole() )
1340 ->getDefaultModel(
$title );
1349 use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
1355 $mainSlotRow->format_name,
1365 $mainSlotRow->slot_content_id =
1366 function (
SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
1419 if ( $blobData !==
null ) {
1420 Assert::parameterType(
'string', $blobData,
'$blobData' );
1421 Assert::parameterType(
'string|null', $blobFlags,
'$blobFlags' );
1425 if ( $blobFlags ===
null ) {
1429 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1430 if ( $data ===
false ) {
1432 "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1440 $data = $this->blobStore->getBlob( $address, $queryFlags );
1443 "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1449 $handler = ContentHandler::getForModelID( $slot->
getModel() );
1451 $content = $handler->unserializeContent( $data, $blobFormat );
1491 $title = Title::newFromLinkTarget( $linkTarget );
1493 'page_namespace' =>
$title->getNamespace(),
1494 'page_title' =>
$title->getDBkey()
1502 $conds[
'rev_id'] = $revId;
1512 $conds[] =
'rev_id=page_latest';
1536 $conds = [
'page_id' => $pageId ];
1543 $conds[
'rev_id'] = $revId;
1553 $conds[] =
'rev_id=page_latest';
1575 'rev_timestamp' => $db->timestamp( $timestamp ),
1576 'page_namespace' =>
$title->getNamespace(),
1577 'page_title' =>
$title->getDBkey()
1601 'slot_revision_id' => $revId,
1630 $slotContents =
null
1634 foreach ( $slotRows as $row ) {
1636 if ( !isset( $row->role_name ) ) {
1637 $row->role_name = $this->slotRoleStore->getName( (
int)$row->slot_role_id );
1640 if ( !isset( $row->model_name ) ) {
1641 if ( isset( $row->content_model ) ) {
1642 $row->model_name = $this->contentModelStore->getName( (
int)$row->content_model );
1646 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1647 $row->model_name = $slotRoleHandler->getDefaultModel(
$title );
1651 if ( !isset( $row->content_id ) && isset( $row->rev_text_id ) ) {
1652 $row->slot_content_id
1657 if ( isset( $row->blob_data ) ) {
1658 $slotContents[$row->content_address] = $row->blob_data;
1661 $contentCallback =
function (
SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1663 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1664 $blob = $slotContents[$slot->getAddress()];
1672 $slots[$row->role_name] =
new SlotRecord( $row, $contentCallback );
1677 'Main slot of revision ' . $revId .
' not found in database!'
1747 array $overrides = []
1749 Assert::parameterType(
'object', $row,
'$row' );
1752 Assert::parameterType(
'integer', $queryFlags,
'$queryFlags' );
1754 if ( !
$title && isset( $overrides[
'title'] ) ) {
1755 if ( !( $overrides[
'title'] instanceof
Title ) ) {
1756 throw new MWException(
'title field override must contain a Title object.' );
1759 $title = $overrides[
'title'];
1762 if ( !isset(
$title ) ) {
1763 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1764 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1766 throw new InvalidArgumentException(
1767 'A Title or ar_namespace and ar_title must be given'
1772 foreach ( $overrides as $key => $value ) {
1774 $row->$field = $value;
1779 $row->ar_user ??
null,
1780 $row->ar_user_text ??
null,
1781 $row->ar_actor ??
null,
1784 }
catch ( InvalidArgumentException $ex ) {
1785 wfWarn( __METHOD__ .
': ' .
$title->getPrefixedDBkey() .
': ' . $ex->getMessage() );
1791 $comment = $this->commentStore->getCommentLegacy( $db,
'ar_comment', $row,
true );
1843 Assert::parameterType(
'object', $row,
'$row' );
1846 $pageId = $row->rev_page ?? 0;
1847 $revId = $row->rev_id ?? 0;
1852 if ( !isset( $row->page_latest ) ) {
1853 $row->page_latest =
$title->getLatestRevID();
1854 if ( $row->page_latest === 0 &&
$title->exists() ) {
1855 wfWarn(
'Encountered title object in limbo: ID ' .
$title->getArticleID() );
1861 $row->rev_user ??
null,
1862 $row->rev_user_text ??
null,
1863 $row->rev_actor ??
null,
1866 }
catch ( InvalidArgumentException $ex ) {
1867 wfWarn( __METHOD__ .
': ' .
$title->getPrefixedDBkey() .
': ' . $ex->getMessage() );
1873 $comment = $this->commentStore->getCommentLegacy( $db,
'rev_comment', $row,
true );
1882 function ( $revId ) use ( $queryFlags ) {
1886 [
'rev_id' => intval( $revId ) ]
1893 $title, $user, $comment, $row, $slots, $this->dbDomain );
1920 array $options = [],
1927 $pageIdsToFetchTitles = [];
1928 $titlesByPageId = [];
1929 foreach ( $rows as $row ) {
1930 if ( isset( $rowsByRevId[$row->rev_id] ) ) {
1933 "Duplicate rows in newRevisionsFromBatch, rev_id {$row->rev_id}"
1936 if (
$title && $row->rev_page !=
$title->getArticleID() ) {
1937 throw new InvalidArgumentException(
1938 "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
1940 } elseif ( !
$title && !isset( $titlesByPageId[ $row->rev_page ] ) ) {
1941 if ( isset( $row->page_namespace ) && isset( $row->page_title ) &&
1944 isset( $row->page_id ) && $row->rev_page === $row->page_id
1946 $titlesByPageId[ $row->rev_page ] = Title::newFromRow( $row );
1948 $pageIdsToFetchTitles[] = $row->rev_page;
1951 $rowsByRevId[$row->rev_id] = $row;
1954 if ( empty( $rowsByRevId ) ) {
1955 $result->setResult(
true, [] );
1962 } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1963 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1964 foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as
$t ) {
1965 $titlesByPageId[
$t->getArticleID()] =
$t;
1970 $result->setResult(
true,
1971 array_map(
function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
1976 $titlesByPageId[$row->rev_page]
1979 $result->warning(
'internalerror', $e->getMessage() );
1988 'slots' => $options[
'slots'] ??
true,
1989 'blobs' => $options[
'content'] ??
false,
1992 if ( is_array( $slotRowOptions[
'slots'] )
1999 $slotRowsStatus = $this->
getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2001 $result->merge( $slotRowsStatus );
2002 $slotRowsByRevId = $slotRowsStatus->getValue();
2004 $result->setResult(
true, array_map(
function ( $row ) use
2005 ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $result ) {
2006 if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
2009 "Couldn't find slots for rev {$row->rev_id}"
2014 return $this->newRevisionFromRowAndSlots(
2017 $this->constructSlotRecords(
2019 $slotRowsByRevId[$row->rev_id],
2021 $titlesByPageId[$row->rev_page]
2025 $titlesByPageId[$row->rev_page]
2028 $result->warning(
'internalerror', $e->getMessage() );
2031 }, $rowsByRevId ) );
2059 array $options = [],
2066 foreach ( $rowsOrIds as $row ) {
2067 $revIds[] = is_object( $row ) ? (int)$row->rev_id : (int)$row;
2072 if ( empty( $revIds ) ) {
2073 $result->setResult(
true, [] );
2079 $revIdField = $slotQueryInfo[
'keys'][
'rev_id'];
2080 $slotQueryConds = [ $revIdField => $revIds ];
2082 if ( $readNew && isset( $options[
'slots'] ) && is_array( $options[
'slots'] ) ) {
2083 if ( empty( $options[
'slots'] ) ) {
2085 $result->setResult(
true, array_fill_keys( $revIds, [] ) );
2089 $roleIdField = $slotQueryInfo[
'keys'][
'role_id'];
2090 $slotQueryConds[$roleIdField] = array_map(
function ( $slot_name ) {
2091 return $this->slotRoleStore->getId( $slot_name );
2092 }, $options[
'slots'] );
2096 $slotRows = $db->select(
2097 $slotQueryInfo[
'tables'],
2098 $slotQueryInfo[
'fields'],
2102 $slotQueryInfo[
'joins']
2105 $slotContents =
null;
2106 if ( $options[
'blobs'] ??
false ) {
2107 $blobAddresses = [];
2108 foreach ( $slotRows as $slotRow ) {
2109 $blobAddresses[] = $slotRow->content_address;
2111 $slotContentFetchStatus = $this->blobStore
2112 ->getBlobBatch( $blobAddresses, $queryFlags );
2113 foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2114 $result->warning( $error[
'message'], ...$error[
'params'] );
2116 $slotContents = $slotContentFetchStatus->getValue();
2119 $slotRowsByRevId = [];
2120 foreach ( $slotRows as $slotRow ) {
2121 if ( $slotContents ===
null ) {
2123 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2124 $slotRow->blob_data = $slotContents[$slotRow->content_address];
2128 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2130 $slotRow->blob_data =
null;
2134 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2135 $slotRow->role_name = $this->slotRoleStore->getName( (
int)$slotRow->slot_role_id );
2139 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2140 $slotRow->model_name = $this->contentModelStore->getName( (
int)$slotRow->content_model );
2143 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2146 $result->setResult(
true, $slotRowsByRevId );
2177 [
'slots' => $slots,
'blobs' =>
true ],
2181 if ( $result->isOK() ) {
2183 foreach ( $result->value as $revId => $rowsByRole ) {
2184 foreach ( $rowsByRole as $role => $slotRow ) {
2185 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2188 unset( $result->value[$revId][$role] );
2192 $result->value[$revId][$role] = (object)[
2193 'blob_data' => $slotRow->blob_data,
2194 'model_name' => $slotRow->model_name,
2222 if ( !
$title && isset( $fields[
'title'] ) ) {
2223 if ( !( $fields[
'title'] instanceof
Title ) ) {
2224 throw new MWException(
'title field must contain a Title object.' );
2227 $title = $fields[
'title'];
2231 $pageId = $fields[
'page'] ?? 0;
2232 $revId = $fields[
'id'] ?? 0;
2237 if ( !isset( $fields[
'page'] ) ) {
2238 $fields[
'page'] =
$title->getArticleID( $queryFlags );
2242 if ( !empty( $fields[
'content'] ) && !( $fields[
'content'] instanceof
Content )
2243 && !is_array( $fields[
'content'] )
2246 'content field must contain a Content object or an array of Content objects.'
2250 if ( !empty( $fields[
'text_id'] ) ) {
2252 throw new MWException(
"The text_id field is only available in the pre-MCR schema" );
2255 if ( !empty( $fields[
'content'] ) ) {
2257 "Text already stored in external store (id {$fields['text_id']}), " .
2258 "can't specify content object"
2264 isset( $fields[
'comment'] )
2267 $commentData = $fields[
'comment_data'] ??
null;
2269 if ( $fields[
'comment'] instanceof
Message ) {
2270 $fields[
'comment'] = CommentStoreComment::newUnsavedComment(
2275 $commentText = trim( strval( $fields[
'comment'] ) );
2276 $fields[
'comment'] = CommentStoreComment::newUnsavedComment(
2286 if ( isset( $fields[
'content'] ) && is_array( $fields[
'content'] ) ) {
2288 foreach ( $fields[
'content'] as $role =>
$content ) {
2289 $revision->setContent( $role,
$content );
2293 $revision->setSlot( $mainSlot );
2312 if ( isset( $fields[
'user'] ) &&
2314 ( $this->dbDomain ===
false ||
2315 ( !$fields[
'user']->getId() && !$fields[
'user']->getActorId() ) )
2317 $user = $fields[
'user'];
2321 $fields[
'user'] ??
null,
2322 $fields[
'user_text'] ??
null,
2323 $fields[
'actor'] ??
null,
2326 }
catch ( InvalidArgumentException $ex ) {
2335 $timestamp = isset( $fields[
'timestamp'] )
2336 ? strval( $fields[
'timestamp'] )
2341 if ( isset( $fields[
'page'] ) ) {
2342 $record->
setPageId( intval( $fields[
'page'] ) );
2345 if ( isset( $fields[
'id'] ) ) {
2346 $record->
setId( intval( $fields[
'id'] ) );
2348 if ( isset( $fields[
'parent_id'] ) ) {
2349 $record->
setParentId( intval( $fields[
'parent_id'] ) );
2352 if ( isset( $fields[
'sha1'] ) ) {
2353 $record->
setSha1( $fields[
'sha1'] );
2355 if ( isset( $fields[
'size'] ) ) {
2356 $record->
setSize( intval( $fields[
'size'] ) );
2359 if ( isset( $fields[
'minor_edit'] ) ) {
2360 $record->
setMinorEdit( intval( $fields[
'minor_edit'] ) !== 0 );
2362 if ( isset( $fields[
'deleted'] ) ) {
2366 if ( isset( $fields[
'comment'] ) ) {
2367 Assert::parameterType(
2368 CommentStoreComment::class,
2410 $conds = [
'rev_page' => intval( $pageid ),
'page_id' => intval( $pageid ) ];
2412 $conds[
'rev_id'] = intval( $id );
2414 $conds[] =
'rev_id=page_latest';
2437 $matchId = intval( $id );
2439 $matchId =
'page_latest';
2446 'page_namespace' =>
$title->getNamespace(),
2447 'page_title' =>
$title->getDBkey()
2472 'rev_timestamp' => $db->
timestamp( $timestamp ),
2473 'page_namespace' =>
$title->getNamespace(),
2474 'page_title' =>
$title->getDBkey()
2505 && !( $flags & self::READ_LATEST )
2506 && $lb->hasStreamingReplicaServers()
2507 && $lb->hasOrMadeRecentMasterChanges()
2509 $flags = self::READ_LATEST;
2555 $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2560 throw new MWException(
"DB connection domain '$dbDomain' does not match '$storeDomain'" );
2580 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2581 $options[] =
'FOR UPDATE';
2613 $roleId = $this->slotRoleStore->getId( $role );
2615 'slot_revision_id' => $revId,
2616 'slot_role_id' => $roleId,
2619 $contentId = $db->
selectField(
'slots',
'slot_content_id', $conditions, __METHOD__ );
2621 return $contentId ?:
null;
2660 $ret[
'tables'][] =
'revision';
2661 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2672 $commentQuery = $this->commentStore->getJoin(
'rev_comment' );
2673 $ret[
'tables'] = array_merge( $ret[
'tables'], $commentQuery[
'tables'] );
2674 $ret[
'fields'] = array_merge( $ret[
'fields'], $commentQuery[
'fields'] );
2675 $ret[
'joins'] = array_merge( $ret[
'joins'], $commentQuery[
'joins'] );
2677 $actorQuery = $this->actorMigration->getJoin(
'rev_user' );
2678 $ret[
'tables'] = array_merge( $ret[
'tables'], $actorQuery[
'tables'] );
2679 $ret[
'fields'] = array_merge( $ret[
'fields'], $actorQuery[
'fields'] );
2680 $ret[
'joins'] = array_merge( $ret[
'joins'], $actorQuery[
'joins'] );
2683 $ret[
'fields'][] =
'rev_text_id';
2685 if ( $this->contentHandlerUseDB ) {
2686 $ret[
'fields'][] =
'rev_content_format';
2687 $ret[
'fields'][] =
'rev_content_model';
2691 if ( in_array(
'page', $options,
true ) ) {
2692 $ret[
'tables'][] =
'page';
2693 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2701 $ret[
'joins'][
'page'] = [
'JOIN', [
'page_id = rev_page' ] ];
2704 if ( in_array(
'user', $options,
true ) ) {
2705 $ret[
'tables'][] =
'user';
2706 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2709 $u = $actorQuery[
'fields'][
'rev_user'];
2710 $ret[
'joins'][
'user'] = [
'LEFT JOIN', [
"$u != 0",
"user_id = $u" ] ];
2713 if ( in_array(
'text', $options,
true ) ) {
2715 throw new InvalidArgumentException(
'text table can no longer be joined directly' );
2725 $ret[
'tables'][] =
'text';
2726 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2730 $ret[
'joins'][
'text'] = [
'JOIN', [
'rev_text_id=old_id' ] ];
2766 $ret[
'keys'][
'rev_id'] =
'rev_id';
2768 $ret[
'tables'][] =
'revision';
2770 $ret[
'fields'][
'slot_revision_id'] =
'rev_id';
2771 $ret[
'fields'][
'slot_content_id'] =
'NULL';
2772 $ret[
'fields'][
'slot_origin'] =
'rev_id';
2775 if ( in_array(
'content', $options,
true ) ) {
2776 $ret[
'fields'][
'content_size'] =
'rev_len';
2777 $ret[
'fields'][
'content_sha1'] =
'rev_sha1';
2778 $ret[
'fields'][
'content_address']
2779 = $db->buildConcat( [ $db->addQuotes(
'tt:' ),
'rev_text_id' ] );
2782 $ret[
'fields'][
'rev_text_id'] =
'rev_text_id';
2784 if ( $this->contentHandlerUseDB ) {
2785 $ret[
'fields'][
'model_name'] =
'rev_content_model';
2787 $ret[
'fields'][
'model_name'] =
'NULL';
2791 $ret[
'keys'][
'rev_id'] =
'slot_revision_id';
2792 $ret[
'keys'][
'role_id'] =
'slot_role_id';
2794 $ret[
'tables'][] =
'slots';
2795 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2802 if ( in_array(
'role', $options,
true ) ) {
2805 $ret[
'tables'][] =
'slot_roles';
2806 $ret[
'joins'][
'slot_roles'] = [
'LEFT JOIN', [
'slot_role_id = role_id' ] ];
2807 $ret[
'fields'][] =
'role_name';
2810 if ( in_array(
'content', $options,
true ) ) {
2811 $ret[
'keys'][
'model_id'] =
'content_model';
2813 $ret[
'tables'][] =
'content';
2814 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2820 $ret[
'joins'][
'content'] = [
'JOIN', [
'slot_content_id = content_id' ] ];
2822 if ( in_array(
'model', $options,
true ) ) {
2825 $ret[
'tables'][] =
'content_models';
2826 $ret[
'joins'][
'content_models'] = [
'LEFT JOIN', [
'content_model = model_id' ] ];
2827 $ret[
'fields'][] =
'model_name';
2850 $commentQuery = $this->commentStore->getJoin(
'ar_comment' );
2851 $actorQuery = $this->actorMigration->getJoin(
'ar_user' );
2853 'tables' => [
'archive' ] + $commentQuery[
'tables'] + $actorQuery[
'tables'],
2866 ] + $commentQuery[
'fields'] + $actorQuery[
'fields'],
2867 'joins' => $commentQuery[
'joins'] + $actorQuery[
'joins'],
2871 $ret[
'fields'][] =
'ar_text_id';
2873 if ( $this->contentHandlerUseDB ) {
2874 $ret[
'fields'][] =
'ar_content_format';
2875 $ret[
'fields'][] =
'ar_content_model';
2917 [
'rev_id',
'rev_len' ],
2918 [
'rev_id' => $revIds ],
2922 foreach (
$res as $row ) {
2923 $revLens[$row->rev_id] = intval( $row->rev_len );
2938 $op = $dir ===
'next' ?
'>' :
'<';
2939 $sort = $dir ===
'next' ?
'ASC' :
'DESC';
2955 if ( $ts ===
false ) {
2957 $ts = $db->selectField(
'archive',
'ar_timestamp',
2958 [
'ar_rev_id' => $rev->
getId() ], __METHOD__ );
2959 if ( $ts ===
false ) {
2964 $ts = $db->addQuotes( $db->timestamp( $ts ) );
2966 $revId = $db->selectField(
'revision',
'rev_id',
2969 "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op {$rev->getId()})"
2973 'ORDER BY' =>
"rev_timestamp $sort, rev_id $sort",
2974 'IGNORE INDEX' =>
'rev_timestamp',
2978 if ( $revId ===
false ) {
3001 if ( $flags instanceof
Title ) {
3024 if ( $flags instanceof
Title ) {
3050 # Use page_latest if ID is not given
3051 if ( !$rev->
getId() ) {
3053 'page',
'page_latest',
3059 'revision',
'rev_id',
3060 [
'rev_page' => $rev->
getPageId(),
'rev_id < ' . $rev->
getId() ],
3062 [
'ORDER BY' =>
'rev_id DESC' ]
3065 return intval( $prevId );
3081 if ( $id instanceof
Title ) {
3084 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
3089 $db->selectField(
'revision',
'rev_timestamp', [
'rev_id' => $id ], __METHOD__ );
3091 return ( $timestamp !==
false ) ?
wfTimestamp( TS_MW, $timestamp ) :
false;
3107 [
'revCount' =>
'COUNT(*)' ],
3108 [
'rev_page' => $id ],
3112 return intval( $row->revCount );
3127 $id =
$title->getArticleID();
3163 'rev_user' =>
$revQuery[
'fields'][
'rev_user'],
3166 'rev_page' => $pageId,
3170 [
'ORDER BY' =>
'rev_timestamp ASC',
'LIMIT' => 50 ],
3173 foreach (
$res as $row ) {
3174 if ( $row->rev_user != $userId ) {
3197 $pageId =
$title->getArticleID();
3204 $revId =
$title->getLatestRevID();
3209 'No latest revision known for page ' .
$title->getPrefixedDBkey()
3210 .
' even though it exists with page ID ' . $pageId
3219 $row = $this->cache->getWithSetCallback(
3222 WANObjectCache::TTL_WEEK,
3223 function ( $curValue, &$ttl, array &$setOpts ) use (
3224 $db, $pageId, $revId, &$fromCache
3226 $setOpts += Database::getCacheSetOptions( $db );
3255 return $this->cache->makeGlobalKey(
3256 self::ROW_CACHE_KEY,
3271class_alias( RevisionStore::class,
'MediaWiki\Storage\RevisionStore' );
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
This class handles the logic for the actor table migration.
A content handler knows how do deal with a specific type of content on a wiki page.
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static hasFlags( $bitfield, $flags)
A collection of public static functions to play with IP address and IP ranges.
Exception thrown when an unregistered content model is requested.
The Message class provides methods which fulfil two basic services:
Utility class for creating new RC entries.
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Represents a title within MediaWiki.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
getName()
Get the user name, or the IP of an anonymous user.
getId()
Get the user's ID.
static newFromAnyId( $userId, $userName, $actorId, $dbDomain=false)
Static factory method for creation from an ID, name, and/or actor ID.
Multi-datacenter aware caching interface.
const SCHEMA_COMPAT_READ_NEW
const SCHEMA_COMPAT_READ_BOTH
const SCHEMA_COMPAT_WRITE_OLD
const SCHEMA_COMPAT_READ_OLD
const SCHEMA_COMPAT_WRITE_NEW
Base interface for content objects.
Interface for database access objects.