36use InvalidArgumentException;
51use Psr\Log\LoggerAwareInterface;
52use Psr\Log\LoggerInterface;
53use Psr\Log\NullLogger;
62use Wikimedia\Assert\Assert;
177 Assert::parameterType(
'string|boolean',
$dbDomain,
'$dbDomain' );
188 $this->logger =
new NullLogger();
202 return $this->blobStore->isReadOnly();
218 list( $mode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
230 return $lb->getConnectionRef( $mode, $groups, $this->dbDomain );
247 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
248 if ( !$pageId && !$revId ) {
249 throw new InvalidArgumentException(
'$pageId and $revId cannot both be 0 or null' );
254 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
255 $queryFlags = self::READ_NORMAL;
258 $canUseTitleNewFromId = ( $pageId !==
null && $pageId > 0 && $this->dbDomain === false );
259 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
262 if ( $canUseTitleNewFromId ) {
263 $titleFlags = ( $dbMode ==
DB_MASTER ? Title::READ_LATEST : 0 );
265 $title = Title::newFromID( $pageId, $titleFlags );
272 $canUseRevId = ( $revId !==
null && $revId > 0 );
274 if ( $canUseRevId ) {
277 $row =
$dbr->selectRow(
278 [
'revision',
'page' ],
287 [
'rev_id' => $revId ],
290 [
'page' => [
'JOIN',
'page_id=rev_page' ] ]
294 return Title::newFromRow( $row );
303 __METHOD__ .
' fell back to READ_LATEST and got a Title.',
311 "Could not determine title for page ID $pageId and revision ID $revId"
323 if ( $value ===
null ) {
325 "$name must not be " . var_export( $value,
true ) .
"!"
340 if ( $value ===
null || $value === 0 || $value ===
'' ) {
342 "$name must not be " . var_export( $value,
true ) .
"!"
369 'main slot must be provided'
379 $this->
failOnNull( $user->getId(),
'user field' );
380 $this->
failOnEmpty( $user->getName(),
'user_text field' );
392 Assert::precondition(
393 $mainSlot->getSize() === $rev->
getSize(),
394 'The revisions\'s size must match the main slot\'s size (see T239717)'
396 Assert::precondition(
397 $mainSlot->getSha1() === $rev->
getSha1(),
398 'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
413 function (
IDatabase $dbw, $fname ) use (
434 Assert::postcondition( $rev->
getId() > 0,
'revision must have an ID' );
435 Assert::postcondition( $rev->
getPageId() > 0,
'revision must have a page ID' );
436 Assert::postcondition(
438 'revision must have a comment'
440 Assert::postcondition(
442 'revision must have a user'
451 foreach ( $slotRoles as $role ) {
453 Assert::postcondition(
454 $slot->getContent() !==
null,
455 $role .
' slot must have content'
457 Assert::postcondition(
458 $slot->hasRevision(),
459 $role .
' slot must have a revision associated'
463 $this->hookRunner->onRevisionRecordInserted( $rev );
466 if ( $this->hookContainer->isRegistered(
'RevisionInsertComplete' ) ) {
468 $legacyRevision =
new Revision( $rev );
469 $this->hookRunner->onRevisionInsertComplete( $legacyRevision,
null,
null );
493 $revisionId = $revisionRow[
'rev_id'];
502 foreach ( $slotRoles as $role ) {
512 if ( $slot->hasRevision() && $slot->hasContentId() ) {
515 $slot->getRevision() === $revisionId,
516 'slot role ' . $slot->getRole(),
517 'Existing slot should belong to revision '
518 . $revisionId .
', but belongs to revision ' . $slot->getRevision() .
'!'
524 $newSlots[$role] = $slot;
526 $newSlots[$role] = $this->
insertSlotOn( $dbw, $revisionId, $slot,
$title, $blobHints );
536 (
object)$revisionRow,
557 array $blobHints = []
598 if ( $user->
getId() === 0 && IPUtils::isValid( $user->
getName() ) ) {
600 'ipc_rev_id' => $revisionId,
602 'ipc_hex' => IPUtils::toHex( $user->
getName() ),
604 $dbw->
insert(
'ip_changes', $ipcRow, __METHOD__ );
627 list( $commentFields, $commentCallback ) =
628 $this->commentStore->insertWithTempTable(
633 $revisionRow += $commentFields;
635 list( $actorFields, $actorCallback ) =
636 $this->actorMigration->getInsertValuesWithTempTable(
641 $revisionRow += $actorFields;
643 $dbw->
insert(
'revision', $revisionRow, __METHOD__ );
645 if ( !isset( $revisionRow[
'rev_id'] ) ) {
647 $revisionRow[
'rev_id'] = intval( $dbw->
insertId() );
649 if ( $dbw->
getType() ===
'mysql' ) {
654 $maxRevId = intval( $dbw->
selectField(
'archive',
'MAX(ar_rev_id)',
'', __METHOD__ ) );
656 $maxRevId2 = intval( $dbw->
selectField(
'slots',
'MAX(slot_revision_id)',
'', __METHOD__ ) );
657 if ( $maxRevId2 >= $maxRevId ) {
658 $maxRevId = $maxRevId2;
662 if ( $maxRevId >= $revisionRow[
'rev_id'] ) {
663 $this->logger->debug(
664 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
665 .
' Trying to fix it.',
667 'revid' => $revisionRow[
'rev_id'],
669 'maxrevid' => $maxRevId,
673 if ( !$dbw->
lock(
'fix-for-T202032', __METHOD__ ) ) {
674 throw new MWException(
'Failed to get database lock for T202032' );
678 function ( $trigger,
IDatabase $dbw ) use ( $fname ) {
679 $dbw->
unlock(
'fix-for-T202032', $fname );
684 $dbw->
delete(
'revision', [
'rev_id' => $revisionRow[
'rev_id'] ], __METHOD__ );
696 $dbw->
selectSQLText(
'archive', [
'v' =>
"MAX(ar_rev_id)" ],
'', __METHOD__ ) .
' FOR UPDATE',
701 $dbw->
selectSQLText(
'slots', [
'v' =>
"MAX(slot_revision_id)" ],
'', __METHOD__ )
708 $row1 ? intval( $row1->v ) : 0,
709 $row2 ? intval( $row2->v ) : 0
715 $revisionRow[
'rev_id'] = $maxRevId + 1;
716 $dbw->
insert(
'revision', $revisionRow, __METHOD__ );
721 $commentCallback( $revisionRow[
'rev_id'] );
722 $actorCallback( $revisionRow[
'rev_id'], $revisionRow );
746 'rev_parent_id' => $parentId,
747 'rev_minor_edit' => $rev->
isMinor() ? 1 : 0,
754 if ( $rev->
getId() !==
null ) {
756 $revisionRow[
'rev_id'] = $rev->
getId();
773 array $blobHints = []
776 $format =
$content->getDefaultFormat();
781 return $this->blobStore->storeBlob(
806 'slot_revision_id' => $revisionId,
807 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->
getRole() ),
808 'slot_content_id' => $contentId,
813 $dbw->
insert(
'slots', $slotRow, __METHOD__ );
824 'content_size' => $slot->
getSize(),
825 'content_sha1' => $slot->
getSha1(),
826 'content_model' => $this->contentModelStore->acquireId( $slot->
getModel() ),
827 'content_address' => $blobAddress,
829 $dbw->
insert(
'content', $contentRow, __METHOD__ );
847 $format =
$content->getDefaultFormat();
848 $handler =
$content->getContentHandler();
852 if ( !$handler->isSupportedFormat( $format ) ) {
853 throw new MWException(
"Can't use format $format with content model $model on $name" );
858 "New content for $name is not valid! Content model is $model"
897 $pageId =
$title->getArticleID();
905 [
'page_id' => $pageId ],
910 if ( !$pageLatest ) {
917 [
'rev_id' => intval( $pageLatest ) ],
922 if ( !$oldRevision ) {
923 $msg =
"Failed to load latest revision ID $pageLatest of page ID $pageId.";
924 $this->logger->error(
926 [
'exception' =>
new RuntimeException( $msg ) ]
932 $timestamp = MWTimestamp::now( TS_MW );
935 $newRevision->setComment( $comment );
936 $newRevision->setUser( $user );
937 $newRevision->setTimestamp( $timestamp );
938 $newRevision->setMinorEdit( $minor );
954 if ( $rc && $rc->getAttribute(
'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
955 return $rc->getAttribute(
'rc_id' );
975 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
977 $rc = RecentChange::newFromConds(
978 [
'rc_this_oldid' => $rev->
getId() ],
1013 if ( $blobData !==
null ) {
1014 Assert::parameterType(
'string', $blobData,
'$blobData' );
1015 Assert::parameterType(
'string|null', $blobFlags,
'$blobFlags' );
1019 if ( $blobFlags ===
null ) {
1023 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
1024 if ( $data ===
false ) {
1026 "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
1034 $data = $this->blobStore->getBlob( $address, $queryFlags );
1037 "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
1042 return $this->contentHandlerFactory
1043 ->getContentHandler( $slot->
getModel() )
1044 ->unserializeContent( $data, $blobFormat );
1084 'page_title' => $linkTarget->
getDBkey()
1089 $title = $this->dbDomain ===
false ? Title::newFromLinkTarget( $linkTarget ) :
null;
1097 $conds[
'rev_id'] = $revId;
1107 $conds[] =
'rev_id=page_latest';
1131 $conds = [
'page_id' => $pageId ];
1138 $conds[
'rev_id'] = $revId;
1148 $conds[] =
'rev_id=page_latest';
1173 int $flags = IDBAccessObject::READ_NORMAL
1178 'rev_timestamp' => $db->timestamp( $timestamp ),
1179 'page_namespace' =>
$title->getNamespace(),
1180 'page_title' =>
$title->getDBkey()
1183 Title::newFromLinkTarget(
$title )
1195 $revQuery = self::getSlotsQueryInfo( [
'content' ] );
1197 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
1198 $db = $this->getDBConnectionRef( $dbMode );
1204 'slot_revision_id' => $revId,
1211 if ( !
$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1213 $this->logger->info(
1214 __METHOD__ .
' falling back to READ_LATEST.',
1220 return $this->loadSlotRecords(
1222 $queryFlags | self::READ_LATEST,
1227 $slots = $this->constructSlotRecords( $revId,
$res, $queryFlags,
$title );
1249 $slotContents =
null
1253 foreach ( $slotRows as $row ) {
1255 if ( !isset( $row->role_name ) ) {
1256 $row->role_name = $this->slotRoleStore->getName( (
int)$row->slot_role_id );
1259 if ( !isset( $row->model_name ) ) {
1260 if ( isset( $row->content_model ) ) {
1261 $row->model_name = $this->contentModelStore->getName( (
int)$row->content_model );
1265 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1266 $row->model_name = $slotRoleHandler->getDefaultModel(
$title );
1271 if ( isset( $row->blob_data ) ) {
1272 $slotContents[$row->content_address] = $row->blob_data;
1275 $contentCallback =
function (
SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1277 if ( isset( $slotContents[$slot->getAddress()] ) ) {
1278 $blob = $slotContents[$slot->getAddress()];
1283 return $this->loadSlotContent( $slot,
$blob,
null,
null, $queryFlags );
1286 $slots[$row->role_name] =
new SlotRecord( $row, $contentCallback );
1290 $this->logger->error(
1291 __METHOD__ .
': Main slot of revision not found in database. See T212428.',
1294 'queryFlags' => $queryFlags,
1300 'Main slot of revision not found in database. See T212428.'
1331 $this->constructSlotRecords( $revId, $slotRows, $queryFlags,
$title )
1338 return $this->loadSlotRecords( $revId, $queryFlags,
$title );
1366 array $overrides = []
1368 return $this->newRevisionFromArchiveRowAndSlots( $row,
null, $queryFlags,
$title, $overrides );
1390 return $this->newRevisionFromRowAndSlots( $row,
null, $queryFlags,
$title, $fromCache );
1417 array $overrides = []
1419 Assert::parameterType( \stdClass::class, $row,
'$row' );
1422 Assert::parameterType(
'integer', $queryFlags,
'$queryFlags' );
1424 if ( !
$title && isset( $overrides[
'title'] ) ) {
1425 if ( !( $overrides[
'title'] instanceof
Title ) ) {
1426 throw new MWException(
'title field override must contain a Title object.' );
1429 $title = $overrides[
'title'];
1432 if ( !isset(
$title ) ) {
1433 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1434 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1436 throw new InvalidArgumentException(
1437 'A Title or ar_namespace and ar_title must be given'
1442 foreach ( $overrides as $key => $value ) {
1444 $row->$field = $value;
1449 $row->ar_user ??
null,
1450 $row->ar_user_text ??
null,
1451 $row->ar_actor ??
null,
1454 }
catch ( InvalidArgumentException $ex ) {
1455 wfWarn( __METHOD__ .
': ' .
$title->getPrefixedDBkey() .
': ' . $ex->getMessage() );
1459 if ( $user->getName() ===
'' ) {
1465 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1467 $comment = $this->commentStore->getCommentLegacy( $db,
'ar_comment', $row,
true );
1470 $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $slots, $queryFlags,
$title );
1500 Assert::parameterType( \stdClass::class, $row,
'$row' );
1503 $pageId = (int)( $row->rev_page ?? 0 );
1504 $revId = (int)( $row->rev_id ?? 0 );
1508 $this->ensureRevisionRowMatchesTitle( $row,
$title );
1511 if ( !isset( $row->page_latest ) ) {
1512 $row->page_latest =
$title->getLatestRevID();
1513 if ( $row->page_latest === 0 &&
$title->exists() ) {
1514 wfWarn(
'Encountered title object in limbo: ID ' .
$title->getArticleID() );
1520 $row->rev_user ??
null,
1521 $row->rev_user_text ??
null,
1522 $row->rev_actor ??
null,
1525 }
catch ( InvalidArgumentException $ex ) {
1526 wfWarn( __METHOD__ .
': ' .
$title->getPrefixedDBkey() .
': ' . $ex->getMessage() );
1530 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1532 $comment = $this->commentStore->getCommentLegacy( $db,
'rev_comment', $row,
true );
1535 $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags,
$title );
1541 function ( $revId ) use ( $queryFlags ) {
1542 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1543 return $this->fetchRevisionRowFromConds(
1545 [
'rev_id' => intval( $revId ) ]
1548 $title, $user, $comment, $row, $slots, $this->dbDomain
1552 $title, $user, $comment, $row, $slots, $this->dbDomain );
1567 $revId = (int)( $row->rev_id ?? 0 );
1568 $revPageId = (int)( $row->rev_page ?? 0 );
1569 $titlePageId =
$title->getArticleID();
1572 if ( $revPageId && $titlePageId && $revPageId !== $titlePageId ) {
1573 $masterPageId =
$title->getArticleID( Title::READ_LATEST );
1574 $masterLatest =
$title->getLatestRevID( Title::READ_LATEST );
1576 if ( $revPageId === $masterPageId ) {
1577 $this->logger->warning(
1578 "Encountered stale Title object",
1580 'page_id_stale' => $titlePageId,
1581 'page_id_reloaded' => $masterPageId,
1582 'page_latest' => $masterLatest,
1588 throw new InvalidArgumentException(
1589 "Revision $revId belongs to page ID $revPageId, "
1590 .
"the provided Title object belongs to page ID $masterPageId"
1623 array $options = [],
1628 $archiveMode = $options[
'archive'] ??
false;
1630 if ( $archiveMode ) {
1631 $revIdField =
'ar_rev_id';
1633 $revIdField =
'rev_id';
1637 $pageIdsToFetchTitles = [];
1638 $titlesByPageKey = [];
1639 foreach ( $rows as $row ) {
1640 if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1642 'internalerror_info',
1643 "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1649 $archiveMode ? $row->ar_namespace .
':' . $row->ar_title : $row->rev_page;
1652 if ( !$archiveMode && $row->rev_page !=
$title->getArticleID() ) {
1653 throw new InvalidArgumentException(
1654 "Revision {$row->$revIdField} doesn't belong to page "
1660 && ( $row->ar_namespace !=
$title->getNamespace()
1661 || $row->ar_title !==
$title->getDBkey() )
1663 throw new InvalidArgumentException(
1664 "Revision {$row->$revIdField} doesn't belong to page "
1665 .
$title->getPrefixedDBkey()
1668 } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1669 if ( isset( $row->page_namespace ) && isset( $row->page_title )
1672 && isset( $row->page_id ) && isset( $row->rev_page )
1673 && $row->rev_page === $row->page_id
1675 $titlesByPageKey[ $row->_page_key ] = Title::newFromRow( $row );
1676 } elseif ( $archiveMode ) {
1678 $titlesByPageKey[ $row->_page_key ] =
1679 Title::makeTitle( $row->ar_namespace, $row->ar_title );
1681 $pageIdsToFetchTitles[] = $row->rev_page;
1684 $rowsByRevId[$row->$revIdField] = $row;
1687 if ( empty( $rowsByRevId ) ) {
1688 $result->setResult(
true, [] );
1695 $pageKey = $archiveMode
1697 :
$title->getArticleID();
1699 $titlesByPageKey[$pageKey] =
$title;
1700 } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1703 Assert::invariant( !$archiveMode,
'Titles are not loaded by ID in archive mode.' );
1705 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
1706 foreach ( Title::newFromIDs( $pageIdsToFetchTitles ) as
$t ) {
1707 $titlesByPageKey[
$t->getArticleID()] =
$t;
1712 $newRevisionRecord = [
1714 $archiveMode ?
'newRevisionFromArchiveRowAndSlots' :
'newRevisionFromRowAndSlots'
1717 if ( !isset( $options[
'slots'] ) ) {
1722 use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
1724 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1726 'internalerror_info',
1727 "Couldn't find title for rev {$row->$revIdField} "
1728 .
"(page key {$row->_page_key})"
1732 return $newRevisionRecord( $row,
null, $queryFlags,
1733 $titlesByPageKey[ $row->_page_key ] );
1735 $result->warning(
'internalerror_info', $e->getMessage() );
1746 'slots' => $options[
'slots'] ??
true,
1747 'blobs' => $options[
'content'] ??
false,
1750 if ( is_array( $slotRowOptions[
'slots'] )
1757 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
1759 $result->merge( $slotRowsStatus );
1760 $slotRowsByRevId = $slotRowsStatus->getValue();
1766 use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
1767 $revIdField, $newRevisionRecord
1769 if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
1771 'internalerror_info',
1772 "Couldn't find slots for rev {$row->$revIdField}"
1776 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
1778 'internalerror_info',
1779 "Couldn't find title for rev {$row->$revIdField} "
1780 .
"(page key {$row->_page_key})"
1785 return $newRevisionRecord(
1788 $this->constructSlotRecords(
1790 $slotRowsByRevId[$row->$revIdField],
1792 $titlesByPageKey[$row->_page_key]
1796 $titlesByPageKey[$row->_page_key]
1799 $result->warning(
'internalerror_info', $e->getMessage() );
1834 array $options = [],
1840 foreach ( $rowsOrIds as $row ) {
1841 if ( is_object( $row ) ) {
1842 $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (int)$row->rev_id;
1844 $revIds[] = (int)$row;
1850 if ( empty( $revIds ) ) {
1851 $result->setResult(
true, [] );
1856 $slotQueryInfo = self::getSlotsQueryInfo( [
'content' ] );
1857 $revIdField = $slotQueryInfo[
'keys'][
'rev_id'];
1858 $slotQueryConds = [ $revIdField => $revIds ];
1860 if ( isset( $options[
'slots'] ) && is_array( $options[
'slots'] ) ) {
1861 if ( empty( $options[
'slots'] ) ) {
1863 $result->setResult(
true, array_fill_keys( $revIds, [] ) );
1867 $roleIdField = $slotQueryInfo[
'keys'][
'role_id'];
1868 $slotQueryConds[$roleIdField] = array_map(
function ( $slot_name ) {
1869 return $this->slotRoleStore->getId( $slot_name );
1870 }, $options[
'slots'] );
1873 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1874 $slotRows = $db->select(
1875 $slotQueryInfo[
'tables'],
1876 $slotQueryInfo[
'fields'],
1880 $slotQueryInfo[
'joins']
1883 $slotContents =
null;
1884 if ( $options[
'blobs'] ??
false ) {
1885 $blobAddresses = [];
1886 foreach ( $slotRows as $slotRow ) {
1887 $blobAddresses[] = $slotRow->content_address;
1889 $slotContentFetchStatus = $this->blobStore
1890 ->getBlobBatch( $blobAddresses, $queryFlags );
1891 foreach ( $slotContentFetchStatus->getErrors() as $error ) {
1892 $result->warning( $error[
'message'], ...$error[
'params'] );
1894 $slotContents = $slotContentFetchStatus->getValue();
1897 $slotRowsByRevId = [];
1898 foreach ( $slotRows as $slotRow ) {
1899 if ( $slotContents ===
null ) {
1901 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
1902 $slotRow->blob_data = $slotContents[$slotRow->content_address];
1905 'internalerror_info',
1906 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
1908 $slotRow->blob_data =
null;
1912 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
1913 $slotRow->role_name = $this->slotRoleStore->getName( (
int)$slotRow->slot_role_id );
1917 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
1918 $slotRow->model_name = $this->contentModelStore->getName( (
int)$slotRow->content_model );
1921 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
1924 $result->setResult(
true, $slotRowsByRevId );
1953 $result = $this->getSlotRowsForBatch(
1955 [
'slots' => $slots,
'blobs' =>
true ],
1959 if ( $result->isOK() ) {
1961 foreach ( $result->value as $revId => $rowsByRole ) {
1962 foreach ( $rowsByRole as $role => $slotRow ) {
1963 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
1966 unset( $result->value[$revId][$role] );
1970 $result->value[$revId][$role] = (object)[
1971 'blob_data' => $slotRow->blob_data,
1972 'model_name' => $slotRow->model_name,
2000 if ( !
$title && isset( $fields[
'title'] ) ) {
2001 if ( !( $fields[
'title'] instanceof
Title ) ) {
2002 throw new MWException(
'title field must contain a Title object.' );
2005 $title = $fields[
'title'];
2009 $pageId = $fields[
'page'] ?? 0;
2010 $revId = $fields[
'id'] ?? 0;
2015 if ( !isset( $fields[
'page'] ) ) {
2016 $fields[
'page'] =
$title->getArticleID( $queryFlags );
2020 if ( !empty( $fields[
'content'] ) && !( $fields[
'content'] instanceof
Content )
2021 && !is_array( $fields[
'content'] )
2024 'content field must contain a Content object or an array of Content objects.'
2028 if ( !empty( $fields[
'text_id'] ) ) {
2029 throw new MWException(
'The text_id field can not be used in MediaWiki 1.35 and later' );
2033 isset( $fields[
'comment'] )
2036 $commentData = $fields[
'comment_data'] ??
null;
2038 if ( $fields[
'comment'] instanceof
Message ) {
2039 $fields[
'comment'] = CommentStoreComment::newUnsavedComment(
2044 $commentText = trim( strval( $fields[
'comment'] ) );
2045 $fields[
'comment'] = CommentStoreComment::newUnsavedComment(
2055 if ( isset( $fields[
'content'] ) ) {
2056 if ( is_array( $fields[
'content'] ) ) {
2057 $slotContent = $fields[
'content'];
2061 } elseif ( isset( $fields[
'text'] ) ) {
2062 if ( isset( $fields[
'content_model'] ) ) {
2063 $model = $fields[
'content_model'];
2065 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler(
SlotRecord::MAIN );
2066 $model = $slotRoleHandler->getDefaultModel(
$title );
2069 $contentHandler = ContentHandler::getForModelID( $model );
2070 $content = $contentHandler->unserializeContent( $fields[
'text'] );
2076 foreach ( $slotContent as $role =>
$content ) {
2077 $revision->setContent( $role,
$content );
2080 $this->initializeMutableRevisionFromArray( $revision, $fields );
2098 if ( isset( $fields[
'user'] ) &&
2100 ( $this->dbDomain ===
false ||
2101 ( !$fields[
'user']->getId() && !$fields[
'user']->getActorId() ) )
2103 $user = $fields[
'user'];
2105 $userID = isset( $fields[
'user'] ) && is_numeric( $fields[
'user'] ) ? $fields[
'user'] :
null;
2109 $fields[
'user_text'] ??
null,
2110 $fields[
'actor'] ??
null,
2113 }
catch ( InvalidArgumentException $ex ) {
2122 $timestamp = isset( $fields[
'timestamp'] )
2123 ? strval( $fields[
'timestamp'] )
2124 : MWTimestamp::now( TS_MW );
2128 if ( isset( $fields[
'page'] ) ) {
2129 $record->
setPageId( intval( $fields[
'page'] ) );
2132 if ( isset( $fields[
'id'] ) ) {
2133 $record->
setId( intval( $fields[
'id'] ) );
2135 if ( isset( $fields[
'parent_id'] ) ) {
2136 $record->
setParentId( intval( $fields[
'parent_id'] ) );
2139 if ( isset( $fields[
'sha1'] ) ) {
2140 $record->
setSha1( $fields[
'sha1'] );
2143 if ( isset( $fields[
'size'] ) ) {
2144 $record->
setSize( intval( $fields[
'size'] ) );
2145 } elseif ( isset( $fields[
'len'] ) ) {
2146 $record->
setSize( intval( $fields[
'len'] ) );
2149 if ( isset( $fields[
'minor_edit'] ) ) {
2150 $record->
setMinorEdit( intval( $fields[
'minor_edit'] ) !== 0 );
2152 if ( isset( $fields[
'deleted'] ) ) {
2156 if ( isset( $fields[
'comment'] ) ) {
2157 Assert::parameterType(
2158 CommentStoreComment::class,
2182 $conds = [
'rev_page' => intval( $pageid ),
'page_id' => intval( $pageid ) ];
2184 $conds[
'rev_id'] = intval( $id );
2186 $conds[] =
'rev_id=page_latest';
2188 return $this->loadRevisionFromConds( $db, $conds );
2211 $matchId = intval( $id );
2213 $matchId =
'page_latest';
2216 return $this->loadRevisionFromConds(
2220 'page_namespace' =>
$title->getNamespace(),
2221 'page_title' =>
$title->getDBkey()
2244 return $this->loadRevisionFromConds( $db,
2246 'rev_timestamp' => $db->
timestamp( $timestamp ),
2247 'page_namespace' =>
$title->getNamespace(),
2248 'page_title' =>
$title->getDBkey()
2273 int $flags = IDBAccessObject::READ_NORMAL,
2277 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2278 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags,
$title, $options );
2280 $lb = $this->getDBLoadBalancer();
2285 && !( $flags & self::READ_LATEST )
2286 && $lb->hasStreamingReplicaServers()
2287 && $lb->hasOrMadeRecentMasterChanges()
2289 $flags = self::READ_LATEST;
2290 $dbw = $this->getDBConnectionRef(
DB_MASTER );
2291 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags,
$title, $options );
2314 int $flags = IDBAccessObject::READ_NORMAL,
2318 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2320 $rev = $this->newRevisionFromRow( $row, $flags,
$title );
2337 $storeDomain = $this->loadBalancer->resolveDomainID( $this->dbDomain );
2338 if ( $dbDomain === $storeDomain ) {
2342 throw new MWException(
"DB connection domain '$dbDomain' does not match '$storeDomain'" );
2361 int $flags = IDBAccessObject::READ_NORMAL,
2364 $this->checkDatabaseDomain( $db );
2366 $revQuery = $this->getQueryInfo( [
'page',
'user' ] );
2367 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2368 $options[] =
'FOR UPDATE';
2408 $ret[
'tables'][] =
'revision';
2409 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2420 $commentQuery = $this->commentStore->getJoin(
'rev_comment' );
2421 $ret[
'tables'] = array_merge( $ret[
'tables'], $commentQuery[
'tables'] );
2422 $ret[
'fields'] = array_merge( $ret[
'fields'], $commentQuery[
'fields'] );
2423 $ret[
'joins'] = array_merge( $ret[
'joins'], $commentQuery[
'joins'] );
2425 $actorQuery = $this->actorMigration->getJoin(
'rev_user' );
2426 $ret[
'tables'] = array_merge( $ret[
'tables'], $actorQuery[
'tables'] );
2427 $ret[
'fields'] = array_merge( $ret[
'fields'], $actorQuery[
'fields'] );
2428 $ret[
'joins'] = array_merge( $ret[
'joins'], $actorQuery[
'joins'] );
2430 if ( in_array(
'page', $options,
true ) ) {
2431 $ret[
'tables'][] =
'page';
2432 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2440 $ret[
'joins'][
'page'] = [
'JOIN', [
'page_id = rev_page' ] ];
2443 if ( in_array(
'user', $options,
true ) ) {
2444 $ret[
'tables'][] =
'user';
2445 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2448 $u = $actorQuery[
'fields'][
'rev_user'];
2449 $ret[
'joins'][
'user'] = [
'LEFT JOIN', [
"$u != 0",
"user_id = $u" ] ];
2452 if ( in_array(
'text', $options,
true ) ) {
2453 throw new InvalidArgumentException(
2454 'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2489 $ret[
'keys'][
'rev_id'] =
'slot_revision_id';
2490 $ret[
'keys'][
'role_id'] =
'slot_role_id';
2492 $ret[
'tables'][] =
'slots';
2493 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2500 if ( in_array(
'role', $options,
true ) ) {
2503 $ret[
'tables'][] =
'slot_roles';
2504 $ret[
'joins'][
'slot_roles'] = [
'LEFT JOIN', [
'slot_role_id = role_id' ] ];
2505 $ret[
'fields'][] =
'role_name';
2508 if ( in_array(
'content', $options,
true ) ) {
2509 $ret[
'keys'][
'model_id'] =
'content_model';
2511 $ret[
'tables'][] =
'content';
2512 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2518 $ret[
'joins'][
'content'] = [
'JOIN', [
'slot_content_id = content_id' ] ];
2520 if ( in_array(
'model', $options,
true ) ) {
2523 $ret[
'tables'][] =
'content_models';
2524 $ret[
'joins'][
'content_models'] = [
'LEFT JOIN', [
'content_model = model_id' ] ];
2525 $ret[
'fields'][] =
'model_name';
2547 $commentQuery = $this->commentStore->getJoin(
'ar_comment' );
2548 $actorQuery = $this->actorMigration->getJoin(
'ar_user' );
2550 'tables' => [
'archive' ] + $commentQuery[
'tables'] + $actorQuery[
'tables'],
2563 ] + $commentQuery[
'fields'] + $actorQuery[
'fields'],
2564 'joins' => $commentQuery[
'joins'] + $actorQuery[
'joins'],
2588 [
'rev_id',
'rev_len' ],
2589 [
'rev_id' => $revIds ],
2593 foreach (
$res as $row ) {
2594 $revLens[$row->rev_id] = intval( $row->rev_len );
2614 return $this->getRevisionSizes( $revIds );
2626 $op = $dir ===
'next' ?
'>' :
'<';
2627 $sort = $dir ===
'next' ?
'ASC' :
'DESC';
2639 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
2640 $db = $this->getDBConnectionRef( $dbType, [
'contributions' ] );
2642 $ts = $this->getTimestampFromId( $rev->
getId(), $flags );
2643 if ( $ts ===
false ) {
2645 $ts = $db->selectField(
'archive',
'ar_timestamp',
2646 [
'ar_rev_id' => $rev->
getId() ], __METHOD__ );
2647 if ( $ts ===
false ) {
2652 $dbts = $db->addQuotes( $db->timestamp( $ts ) );
2654 $revId = $db->selectField(
'revision',
'rev_id',
2657 "rev_timestamp $op $dbts OR (rev_timestamp = $dbts AND rev_id $op {$rev->getId()})"
2661 'ORDER BY' => [
"rev_timestamp $sort",
"rev_id $sort" ],
2662 'IGNORE INDEX' =>
'rev_timestamp',
2666 if ( $revId ===
false ) {
2670 return $this->getRevisionById( intval( $revId ) );
2689 if ( $flags instanceof
Title ) {
2695 return $this->getRelativeRevision( $rev, $flags,
'prev' );
2712 if ( $flags instanceof
Title ) {
2718 return $this->getRelativeRevision( $rev, $flags,
'next' );
2733 $this->checkDatabaseDomain( $db );
2738 # Use page_latest if ID is not given
2739 if ( !$rev->
getId() ) {
2741 'page',
'page_latest',
2747 'revision',
'rev_id',
2748 [
'rev_page' => $rev->
getPageId(),
'rev_id < ' . $rev->
getId() ],
2750 [
'ORDER BY' =>
'rev_id DESC' ]
2753 return intval( $prevId );
2769 if ( $id instanceof
Title ) {
2772 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2774 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2777 $db->selectField(
'revision',
'rev_timestamp', [
'rev_id' => $id ], __METHOD__ );
2779 return ( $timestamp !==
false ) ? MWTimestamp::convert( TS_MW, $timestamp ) :
false;
2792 $this->checkDatabaseDomain( $db );
2795 [
'revCount' =>
'COUNT(*)' ],
2796 [
'rev_page' => $id ],
2800 return intval( $row->revCount );
2815 $id =
$title->getArticleID();
2817 return $this->countRevisionsByPageId( $db, $id );
2841 $this->checkDatabaseDomain( $db );
2851 'rev_user' =>
$revQuery[
'fields'][
'rev_user'],
2854 'rev_page' => $pageId,
2858 [
'ORDER BY' =>
'rev_timestamp ASC',
'LIMIT' => 50 ],
2861 foreach (
$res as $row ) {
2862 if ( $row->rev_user != $userId ) {
2883 $db = $this->getDBConnectionRef(
DB_REPLICA );
2885 $revIdPassed = $revId;
2886 $pageId =
$title->getArticleID();
2893 $revId =
$title->getLatestRevID();
2898 'No latest revision known for page ' .
$title->getPrefixedDBkey()
2899 .
' even though it exists with page ID ' . $pageId
2908 $row = $this->cache->getWithSetCallback(
2910 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2911 WANObjectCache::TTL_WEEK,
2912 function ( $curValue, &$ttl, array &$setOpts ) use (
2913 $db, $revId, &$fromCache
2915 $setOpts += Database::getCacheSetOptions( $db );
2916 $row = $this->fetchRevisionRowFromConds( $db, [
'rev_id' => intval( $revId ) ] );
2926 $this->ensureRevisionRowMatchesTitle( $row,
$title, [
2927 'from_cache_flag' => $fromCache,
2928 'page_id_initial' => $pageId,
2929 'rev_id_used' => $revId,
2930 'rev_id_requested' => $revIdPassed,
2933 return $this->newRevisionFromRow( $row, 0,
$title, $fromCache );
2949 int $flags = IDBAccessObject::READ_NORMAL
2952 return $this->newRevisionFromConds(
2954 'page_namespace' =>
$title->getNamespace(),
2955 'page_title' =>
$title->getDBkey()
2960 'ORDER BY' => [
'rev_timestamp ASC',
'rev_id ASC' ],
2961 'IGNORE INDEX' => [
'revision' =>
'rev_timestamp' ],
2978 return $this->cache->makeGlobalKey(
2979 self::ROW_CACHE_KEY,
2995 if ( $rev->getId() ===
null ) {
2996 throw new InvalidArgumentException(
"Unsaved {$paramName} revision passed" );
2998 if ( $rev->getPageId() !== $pageId ) {
2999 throw new InvalidArgumentException(
3000 "Revision {$rev->getId()} doesn't belong to page {$pageId}"
3026 $options = (array)$options;
3029 if ( in_array(
'include_old', $options ) ) {
3032 if ( in_array(
'include_new', $options ) ) {
3035 if ( in_array(
'include_both', $options ) ) {
3042 $oldTs =
$dbr->addQuotes(
$dbr->timestamp( $old->getTimestamp() ) );
3043 $conds[] =
"(rev_timestamp = {$oldTs} AND rev_id {$oldCmp} {$old->getId()}) " .
3044 "OR rev_timestamp > {$oldTs}";
3047 $newTs =
$dbr->addQuotes(
$dbr->timestamp( $new->getTimestamp() ) );
3048 $conds[] =
"(rev_timestamp = {$newTs} AND rev_id {$newCmp} {$new->getId()}) " .
3049 "OR rev_timestamp < {$newTs}";
3086 ?
string $order =
null,
3087 int $flags = IDBAccessObject::READ_NORMAL
3089 $this->assertRevisionParameter(
'old', $pageId, $old );
3090 $this->assertRevisionParameter(
'new', $pageId, $new );
3092 $options = (array)$options;
3093 $includeOld = in_array(
'include_old', $options ) ||
3094 in_array(
'include_both', $options );
3095 $includeNew = in_array(
'include_new', $options ) ||
3096 in_array(
'include_both', $options );
3102 if ( $old && $new && $new->getId() === $old->getId() ) {
3103 return $includeOld || $includeNew ? [ $new->getId() ] : [];
3106 $db = $this->getDBConnectionRefForQueryFlags( $flags );
3107 $conds = array_merge(
3109 'rev_page' => $pageId,
3110 $db->bitAnd(
'rev_deleted', RevisionRecord::DELETED_TEXT ) .
' = 0'
3112 $this->getRevisionLimitConditions( $db, $old, $new, $options )
3116 if ( $order !==
null ) {
3117 $queryOptions[
'ORDER BY'] = [
"rev_timestamp $order",
"rev_id $order" ];
3119 if ( $max !==
null ) {
3120 $queryOptions[
'LIMIT'] = $max + 1;
3123 $values = $db->selectFieldValues(
3130 return array_map(
'intval', $values );
3162 $this->assertRevisionParameter(
'old', $pageId, $old );
3163 $this->assertRevisionParameter(
'new', $pageId, $new );
3164 $options = (array)$options;
3170 if ( $old && $new && $new->getId() === $old->getId() ) {
3171 if ( empty( $options ) ) {
3174 return $user ? [ $new->getUser( RevisionRecord::FOR_PUBLIC, $user ) ] : [ $new->getUser() ];
3179 $conds = array_merge(
3181 'rev_page' => $pageId,
3182 $dbr->bitAnd(
'rev_deleted', RevisionRecord::DELETED_USER ) .
" = 0"
3184 $this->getRevisionLimitConditions(
$dbr, $old, $new, $options )
3187 $queryOpts = [
'DISTINCT' ];
3188 if ( $max !==
null ) {
3189 $queryOpts[
'LIMIT'] = $max + 1;
3192 $actorQuery = $this->actorMigration->getJoin(
'rev_user' );
3193 return array_map(
function ( $row ) {
3194 return new UserIdentityValue( (
int)$row->rev_user, $row->rev_user_text, (
int)$row->rev_actor );
3195 }, iterator_to_array(
$dbr->select(
3196 array_merge( [
'revision' ], $actorQuery[
'tables'] ),
3197 $actorQuery[
'fields'],
3200 $actorQuery[
'joins']
3235 return count( $this->getAuthorsBetween( $pageId, $old, $new, $user, $max, $options ) );
3265 $this->assertRevisionParameter(
'old', $pageId, $old );
3266 $this->assertRevisionParameter(
'new', $pageId, $new );
3272 if ( $old && $new && $new->getId() === $old->getId() ) {
3277 $conds = array_merge(
3279 'rev_page' => $pageId,
3280 $dbr->bitAnd(
'rev_deleted', RevisionRecord::DELETED_TEXT ) .
" = 0"
3282 $this->getRevisionLimitConditions(
$dbr, $old, $new, $options )
3284 if ( $max !==
null ) {
3285 return $dbr->selectRowCount(
'revision',
'1',
3288 [
'LIMIT' => $max + 1 ]
3291 return (
int)
$dbr->selectField(
'revision',
'count(*)', $conds, __METHOD__ );
3302class_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.
wfBacktrace( $raw=null)
Get a debug backtrace as a string.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
This class handles the logic for the actor table migration and should always be used in lieu of direc...
A content handler knows how do deal with a specific type of content on a wiki page.
Helper class for DAO classes.
Library for creating and parsing MW-style timestamps.
Exception thrown when an unregistered content model is requested.
The Message class deals with fetching and processing of interface message into a variety of formats.
Utility class for creating new RC entries.
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.
Base interface for content objects.
Interface for database access objects.