33 use InvalidArgumentException;
60 use Psr\Log\LoggerAwareInterface;
61 use Psr\Log\LoggerInterface;
62 use Psr\Log\NullLogger;
70 use Wikimedia\Assert\Assert;
71 use Wikimedia\IPUtils;
115 private $loadBalancer;
130 private $commentStore;
135 private $actorMigration;
148 private $contentModelStore;
153 private $slotRoleStore;
156 private $slotRoleRegistry;
159 private $contentHandlerFactory;
168 private $titleFactory;
210 $wikiId = WikiAwareEntity::LOCAL
212 Assert::parameterType( [
'string',
'false' ], $wikiId,
'$wikiId' );
214 $this->loadBalancer = $loadBalancer;
215 $this->blobStore = $blobStore;
216 $this->cache = $cache;
217 $this->localCache = $localCache;
218 $this->commentStore = $commentStore;
219 $this->contentModelStore = $contentModelStore;
220 $this->slotRoleStore = $slotRoleStore;
221 $this->slotRoleRegistry = $slotRoleRegistry;
222 $this->actorMigration = $actorMigration;
223 $this->actorStore = $actorStore;
224 $this->wikiId = $wikiId;
225 $this->logger =
new NullLogger();
226 $this->contentHandlerFactory = $contentHandlerFactory;
227 $this->pageStore = $pageStore;
228 $this->titleFactory = $titleFactory;
229 $this->hookRunner =
new HookRunner( $hookContainer );
233 $this->logger = $logger;
240 return $this->blobStore->isReadOnly();
249 return $this->wikiId;
257 private function getDBConnectionRefForQueryFlags( $queryFlags ) {
259 return $this->getDBConnectionRef( $mode );
268 private function getDBConnectionRef( $mode, $groups = [] ) {
269 return $this->loadBalancer->getConnectionRef( $mode, $groups, $this->wikiId );
288 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
290 if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
291 wfDeprecatedMsg(
'Using a Title object to refer to a page on another site.',
'1.36' );
294 $page = $this->getPage( $pageId, $revId, $queryFlags );
296 return $this->titleFactory->castFromPageIdentity( $page );
309 private function getPage( ?
int $pageId, ?
int $revId,
int $queryFlags = self::READ_NORMAL ) {
310 if ( !$pageId && !$revId ) {
311 throw new InvalidArgumentException(
'$pageId and $revId cannot both be 0 or null' );
317 $queryFlags = self::READ_NORMAL;
321 if ( $pageId !==
null && $pageId > 0 ) {
322 $page = $this->pageStore->getPageById( $pageId, $queryFlags );
324 return $this->wrapPage( $page );
329 if ( $revId !==
null && $revId > 0 ) {
330 $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
331 ->join(
'revision',
null,
'page_id=rev_page' )
332 ->conds( [
'rev_id' => $revId ] )
333 ->caller( __METHOD__ );
335 $page = $pageQuery->fetchPageRecord();
337 return $this->wrapPage( $page );
342 if ( $queryFlags === self::READ_NORMAL ) {
343 $title = $this->getPage( $pageId, $revId, self::READ_LATEST );
346 __METHOD__ .
' fell back to READ_LATEST and got a Title.',
347 [
'exception' =>
new RuntimeException() ]
353 throw new RevisionAccessException(
354 'Could not determine title for page ID {page_id} and revision ID {rev_id}',
356 'page_id' => $pageId,
367 private function wrapPage( PageIdentity $page ): PageIdentity {
368 if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
376 return $this->titleFactory->castFromPageIdentity( $page );
389 private function failOnNull( $value, $name ) {
390 if ( $value ===
null ) {
391 throw new IncompleteRevisionException(
392 "$name must not be " . var_export( $value,
true ) .
"!"
406 private function failOnEmpty( $value, $name ) {
407 if ( $value ===
null || $value === 0 || $value ===
'' ) {
408 throw new IncompleteRevisionException(
409 "$name must not be " . var_export( $value,
true ) .
"!"
429 $this->checkDatabaseDomain( $dbw );
436 'main slot must be provided'
441 $this->failOnNull( $rev->
getSize(),
'size field' );
442 $this->failOnEmpty( $rev->
getSha1(),
'sha1 field' );
443 $this->failOnEmpty( $rev->
getTimestamp(),
'timestamp field' );
446 $this->failOnNull( $user->getId(),
'user field' );
447 $this->failOnEmpty( $user->getName(),
'user_text field' );
459 Assert::precondition(
460 $mainSlot->getSize() === $rev->
getSize(),
461 'The revisions\'s size must match the main slot\'s size (see T239717)'
463 Assert::precondition(
464 $mainSlot->getSha1() === $rev->
getSha1(),
465 'The revisions\'s SHA1 hash must match the main slot\'s SHA1 hash (see T239717)'
469 $pageId = $this->failOnEmpty( $rev->
getPageId( $this->wikiId ),
'rev_page field' );
471 $parentId = $rev->
getParentId() ?? $this->getPreviousRevisionId( $dbw, $rev );
476 function (
IDatabase $dbw, $fname ) use (
483 return $this->insertRevisionInternal(
495 Assert::postcondition( $rev->
getId( $this->wikiId ) > 0,
'revision must have an ID' );
496 Assert::postcondition( $rev->
getPageId( $this->wikiId ) > 0,
'revision must have a page ID' );
497 Assert::postcondition(
499 'revision must have a comment'
501 Assert::postcondition(
503 'revision must have a user'
512 foreach ( $slotRoles as $role ) {
514 Assert::postcondition(
516 $role .
' slot must have content'
518 Assert::postcondition(
520 $role .
' slot must have a revision associated'
524 $this->hookRunner->onRevisionRecordInserted( $rev );
546 $this->checkDatabaseDomain( $dbw );
550 Assert::precondition(
551 $this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
552 'Trying to modify a slot that is not derived'
556 $isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
557 Assert::precondition(
559 'Trying to remove a slot that is not derived'
561 throw new LogicException(
'Removing derived slots is not yet implemented. See T277394.' );
567 function ( IDatabase $dbw, $fname ) use (
571 return $this->updateSlotsInternal(
573 $revisionSlotsUpdate,
579 foreach ( $slotRecords as $role => $slot ) {
580 Assert::postcondition(
582 $role .
' slot must have content'
584 Assert::postcondition(
586 $role .
' slot must have a revision associated'
599 private function updateSlotsInternal(
600 RevisionRecord $revision,
601 RevisionSlotsUpdate $revisionSlotsUpdate,
604 $page = $revision->getPage();
605 $revId = $revision->
getId( $this->wikiId );
607 BlobStore::PAGE_HINT => $page->
getId( $this->wikiId ),
608 BlobStore::REVISION_HINT => $revId,
609 BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
613 foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
614 $slot = $revisionSlotsUpdate->getModifiedSlot( $role );
615 $newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
621 private function insertRevisionInternal(
625 CommentStoreComment $comment,
630 $slotRoles = $rev->getSlotRoles();
632 $revisionRow = $this->insertRevisionRowOn(
638 $revisionId = $revisionRow[
'rev_id'];
641 BlobStore::PAGE_HINT => $pageId,
642 BlobStore::REVISION_HINT => $revisionId,
643 BlobStore::PARENT_HINT => $parentId,
647 foreach ( $slotRoles as $role ) {
648 $slot = $rev->getSlot( $role, RevisionRecord::RAW );
661 'slot role ' . $slot->
getRole(),
662 'Existing slot should belong to revision '
663 . $revisionId .
', but belongs to revision ' . $slot->
getRevision() .
'!'
669 $newSlots[$role] = $slot;
671 $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $page, $blobHints );
675 $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
677 $rev =
new RevisionStoreRecord(
681 (
object)$revisionRow,
682 new RevisionSlots( $newSlots ),
697 private function insertSlotOn(
700 SlotRecord $protoSlot,
702 array $blobHints = []
704 if ( $protoSlot->hasAddress() ) {
705 $blobAddress = $protoSlot->getAddress();
707 $blobAddress = $this->storeContentBlob( $protoSlot, $page, $blobHints );
710 if ( $protoSlot->hasContentId() ) {
711 $contentId = $protoSlot->getContentId();
713 $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
716 $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
718 return SlotRecord::newSaved(
733 private function insertIpChangesRow(
739 if ( !$user->isRegistered() && IPUtils::isValid( $user->getName() ) ) {
741 'ipc_rev_id' => $revisionId,
742 'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
743 'ipc_hex' => IPUtils::toHex( $user->getName() ),
745 $dbw->insert(
'ip_changes', $ipcRow, __METHOD__ );
759 private function insertRevisionRowOn(
764 $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $parentId );
766 [ $commentFields, $commentCallback ] =
767 $this->commentStore->insertWithTempTable(
770 $rev->getComment( RevisionRecord::RAW )
772 $revisionRow += $commentFields;
774 [ $actorFields, $actorCallback ] =
775 $this->actorMigration->getInsertValuesWithTempTable(
778 $rev->getUser( RevisionRecord::RAW )
780 $revisionRow += $actorFields;
782 $dbw->insert(
'revision', $revisionRow, __METHOD__ );
784 if ( !isset( $revisionRow[
'rev_id'] ) ) {
786 $revisionRow[
'rev_id'] = intval( $dbw->insertId() );
788 if ( $dbw->getType() ===
'mysql' ) {
793 $maxRevId = intval( $dbw->selectField(
'archive',
'MAX(ar_rev_id)',
'', __METHOD__ ) );
795 $maxRevId2 = intval( $dbw->selectField(
'slots',
'MAX(slot_revision_id)',
'', __METHOD__ ) );
796 if ( $maxRevId2 >= $maxRevId ) {
797 $maxRevId = $maxRevId2;
801 if ( $maxRevId >= $revisionRow[
'rev_id'] ) {
802 $this->logger->debug(
803 '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.'
804 .
' Trying to fix it.',
806 'revid' => $revisionRow[
'rev_id'],
808 'maxrevid' => $maxRevId,
812 if ( !$dbw->lock(
'fix-for-T202032', __METHOD__ ) ) {
813 throw new MWException(
'Failed to get database lock for T202032' );
816 $dbw->onTransactionResolution(
817 static function ( $trigger, IDatabase $dbw ) use ( $fname ) {
818 $dbw->unlock(
'fix-for-T202032', $fname );
823 $dbw->delete(
'revision', [
'rev_id' => $revisionRow[
'rev_id'] ], __METHOD__ );
835 $dbw->selectSQLText(
'archive', [
'v' =>
"MAX(ar_rev_id)" ],
'', __METHOD__ ) .
' FOR UPDATE',
840 $dbw->selectSQLText(
'slots', [
'v' =>
"MAX(slot_revision_id)" ],
'', __METHOD__ )
847 $row1 ? intval( $row1->v ) : 0,
848 $row2 ? intval( $row2->v ) : 0
854 $revisionRow[
'rev_id'] = $maxRevId + 1;
855 $dbw->insert(
'revision', $revisionRow, __METHOD__ );
860 $commentCallback( $revisionRow[
'rev_id'] );
861 $actorCallback( $revisionRow[
'rev_id'], $revisionRow );
873 private function getBaseRevisionRow(
880 'rev_page' => $rev->getPageId( $this->wikiId ),
881 'rev_parent_id' => $parentId,
882 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
883 'rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
884 'rev_deleted' => $rev->getVisibility(),
885 'rev_len' => $rev->getSize(),
886 'rev_sha1' => $rev->getSha1(),
889 if ( $rev->getId( $this->wikiId ) !==
null ) {
891 $revisionRow[
'rev_id'] = $rev->getId( $this->wikiId );
905 private function storeContentBlob(
908 array $blobHints = []
911 $format =
$content->getDefaultFormat();
914 $this->checkContent(
$content, $page, $slot->getRole() );
916 return $this->blobStore->storeBlob(
923 BlobStore::DESIGNATION_HINT =>
'page-content',
924 BlobStore::ROLE_HINT => $slot->getRole(),
925 BlobStore::SHA1_HINT => $slot->getSha1(),
926 BlobStore::MODEL_HINT => $model,
927 BlobStore::FORMAT_HINT => $format,
939 private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
941 'slot_revision_id' => $revisionId,
942 'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
943 'slot_content_id' => $contentId,
946 'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
948 $dbw->insert(
'slots', $slotRow, __METHOD__ );
957 private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
959 'content_size' => $slot->getSize(),
960 'content_sha1' => $slot->getSha1(),
961 'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
962 'content_address' => $blobAddress,
964 $dbw->insert(
'content', $contentRow, __METHOD__ );
965 return intval( $dbw->insertId() );
978 private function checkContent(
Content $content, PageIdentity $page,
string $role ) {
982 $format =
$content->getDefaultFormat();
983 $handler =
$content->getContentHandler();
985 if ( !$handler->isSupportedFormat( $format ) ) {
987 "Can't use format $format with content model $model on $page role $role"
993 "New content for $page role $role is not valid! Content model is $model"
1030 $this->checkDatabaseDomain( $dbw );
1032 $pageId = $this->getArticleId( $page );
1040 [
'page_id' => $pageId ],
1045 if ( !$pageLatest ) {
1046 $msg =
'T235589: Failed to select table row during null revision creation' .
1047 " Page id '$pageId' does not exist.";
1048 $this->logger->error(
1050 [
'exception' =>
new RuntimeException( $msg ) ]
1057 $oldRevision = $this->loadRevisionFromConds(
1059 [
'rev_id' => intval( $pageLatest ) ],
1064 if ( !$oldRevision ) {
1065 $msg =
"Failed to load latest revision ID $pageLatest of page ID $pageId.";
1066 $this->logger->error(
1068 [
'exception' =>
new RuntimeException( $msg ) ]
1074 $timestamp = MWTimestamp::now( TS_MW );
1075 $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
1077 $newRevision->setComment( $comment );
1078 $newRevision->setUser( $user );
1079 $newRevision->setTimestamp( $timestamp );
1080 $newRevision->setMinorEdit( $minor );
1082 return $newRevision;
1095 $rc = $this->getRecentChange( $rev );
1097 return $rc->getAttribute(
'rc_id' );
1120 [
'rc_this_oldid' => $rev->
getId( $this->wikiId ) ],
1148 private function loadSlotContent(
1150 ?
string $blobData =
null,
1151 ?
string $blobFlags =
null,
1152 ?
string $blobFormat =
null,
1155 if ( $blobData !==
null ) {
1158 if ( $blobFlags ===
null ) {
1163 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $blobAddress );
1164 }
catch ( BadBlobException $e ) {
1165 throw new BadRevisionException( $e->getMessage(), [], 0, $e );
1168 if ( $data ===
false ) {
1169 throw new RevisionAccessException(
1170 'Failed to expand blob data using flags {flags} (key: {cache_key})',
1172 'flags' => $blobFlags,
1173 'cache_key' => $blobAddress,
1182 $data = $this->blobStore->getBlob( $address, $queryFlags );
1183 }
catch ( BadBlobException $e ) {
1184 throw new BadRevisionException( $e->getMessage(), [], 0, $e );
1185 }
catch ( BlobAccessException $e ) {
1186 throw new RevisionAccessException(
1187 'Failed to load data blob from {address} for revision {revision}. '
1188 .
'If this problem persist, use the findBadBlobs maintenance script '
1189 .
'to investigate the issue and mark bad blobs.',
1190 [
'address' => $e->getMessage(),
'revision' => $slot->
getRevision() ],
1200 if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
1201 $this->logger->warning(
1202 "Undefined content model '$model', falling back to FallbackContent",
1206 'role_name' => $slot->
getRole(),
1207 'model_name' => $model,
1208 'exception' =>
new RuntimeException()
1215 return $this->contentHandlerFactory
1216 ->getContentHandler( $model )
1217 ->unserializeContent( $data, $blobFormat );
1238 return $this->newRevisionFromConds( [
'rev_id' => intval( $id ) ], $flags, $page );
1259 'page_namespace' => $page->getNamespace(),
1260 'page_title' => $page->getDBkey()
1274 $conds[
'rev_id'] = $revId;
1275 return $this->newRevisionFromConds( $conds, $flags, $page );
1282 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1283 $conds[] =
'rev_id=page_latest';
1284 return $this->loadRevisionFromConds( $db, $conds, $flags, $page );
1305 $conds = [
'page_id' => $pageId ];
1312 $conds[
'rev_id'] = $revId;
1313 return $this->newRevisionFromConds( $conds, $flags );
1320 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1322 $conds[] =
'rev_id=page_latest';
1324 return $this->loadRevisionFromConds( $db, $conds, $flags );
1346 int $flags = IDBAccessObject::READ_NORMAL
1352 $db = $this->getDBConnectionRefForQueryFlags( $flags );
1353 return $this->newRevisionFromConds(
1355 'rev_timestamp' => $db->timestamp( $timestamp ),
1356 'page_namespace' => $page->getNamespace(),
1357 'page_title' => $page->getDBkey()
1371 private function loadSlotRecords( $revId, $queryFlags,
PageIdentity $page ) {
1374 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1375 return $this->constructSlotRecords( $revId,
$res, $queryFlags, $page );
1379 $res = $this->localCache->getWithSetCallback(
1380 $this->localCache->makeKey(
1386 $this->localCache::TTL_HOUR,
1387 function () use ( $revId, $queryFlags, $page ) {
1388 return $this->cache->getWithSetCallback(
1389 $this->cache->makeKey(
1395 WANObjectCache::TTL_DAY,
1396 function () use ( $revId, $queryFlags, $page ) {
1397 $res = $this->loadSlotRecordsFromDb( $revId, $queryFlags, $page );
1411 return $this->constructSlotRecords( $revId,
$res, $queryFlags, $page );
1414 private function loadSlotRecordsFromDb( $revId, $queryFlags, PageIdentity $page ): array {
1415 $revQuery = $this->getSlotsQueryInfo( [
'content' ] );
1418 $db = $this->getDBConnectionRef( $dbMode );
1424 'slot_revision_id' => $revId,
1431 if ( !
$res->numRows() && !( $queryFlags & self::READ_LATEST ) ) {
1433 $this->logger->info(
1434 __METHOD__ .
' falling back to READ_LATEST.',
1437 'exception' =>
new RuntimeException(),
1440 return $this->loadSlotRecordsFromDb(
1442 $queryFlags | self::READ_LATEST,
1446 return iterator_to_array(
$res );
1461 private function constructSlotRecords(
1466 $slotContents =
null
1470 foreach ( $slotRows as $row ) {
1472 if ( !isset( $row->role_name ) ) {
1473 $row->role_name = $this->slotRoleStore->getName( (
int)$row->slot_role_id );
1476 if ( !isset( $row->model_name ) ) {
1477 if ( isset( $row->content_model ) ) {
1478 $row->model_name = $this->contentModelStore->getName( (
int)$row->content_model );
1482 $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( $row->role_name );
1483 $row->model_name = $slotRoleHandler->getDefaultModel( $page );
1488 if ( isset( $row->blob_data ) ) {
1489 $slotContents[$row->content_address] = $row->blob_data;
1492 $contentCallback =
function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
1494 if ( isset( $slotContents[$slot->
getAddress()] ) ) {
1500 return $this->loadSlotContent( $slot,
$blob,
null,
null, $queryFlags );
1503 $slots[$row->role_name] =
new SlotRecord( $row, $contentCallback );
1506 if ( !isset( $slots[SlotRecord::MAIN] ) ) {
1507 $this->logger->error(
1508 __METHOD__ .
': Main slot of revision not found in database. See T212428.',
1511 'queryFlags' => $queryFlags,
1512 'exception' =>
new RuntimeException(),
1516 throw new RevisionAccessException(
1517 'Main slot of revision not found in database. See T212428.'
1539 private function newRevisionSlots(
1547 $slots =
new RevisionSlots(
1548 $this->constructSlotRecords( $revId, $slotRows, $queryFlags, $page )
1551 $slots =
new RevisionSlots(
function () use( $revId, $queryFlags, $page ) {
1552 return $this->loadSlotRecords( $revId, $queryFlags, $page );
1584 array $overrides = []
1586 return $this->newRevisionFromArchiveRowAndSlots( $row,
null, $queryFlags, $page, $overrides );
1607 return $this->newRevisionFromRowAndSlots( $row,
null, $queryFlags, $page, $fromCache );
1632 int $queryFlags = 0,
1634 array $overrides = []
1636 if ( !$page && isset( $overrides[
'title'] ) ) {
1637 if ( !( $overrides[
'title'] instanceof
PageIdentity ) ) {
1638 throw new MWException(
'title field override must contain a PageIdentity object.' );
1641 $page = $overrides[
'title'];
1644 if ( !isset( $page ) ) {
1645 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1648 throw new InvalidArgumentException(
1649 'A Title or ar_namespace and ar_title must be given'
1654 foreach ( $overrides as $key => $value ) {
1656 $row->$field = $value;
1660 $user = $this->actorStore->newActorFromRowFields(
1661 $row->ar_user ??
null,
1662 $row->ar_user_text ??
null,
1663 $row->ar_actor ??
null
1665 }
catch ( InvalidArgumentException $ex ) {
1666 $this->logger->warning(
'Could not load user for archive revision {rev_id}', [
1667 'ar_rev_id' => $row->ar_rev_id,
1668 'ar_actor' => $row->ar_actor ??
'null',
1669 'ar_user_text' => $row->ar_user_text ??
'null',
1670 'ar_user' => $row->ar_user ??
'null',
1673 $user = $this->actorStore->getUnknownActor();
1676 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1678 $comment = $this->commentStore->getCommentLegacy( $db,
'ar_comment', $row,
true );
1681 $slots = $this->newRevisionSlots( (
int)$row->ar_rev_id, $row, $slots, $queryFlags, $page );
1707 int $queryFlags = 0,
1709 bool $fromCache =
false
1712 if ( isset( $row->page_id )
1713 && isset( $row->page_namespace )
1714 && isset( $row->page_title )
1718 (
int)$row->page_namespace,
1723 $page = $this->wrapPage( $page );
1725 $pageId = (int)( $row->rev_page ?? 0 );
1726 $revId = (int)( $row->rev_id ?? 0 );
1728 $page = $this->getPage( $pageId, $revId, $queryFlags );
1731 $page = $this->ensureRevisionRowMatchesPage( $row, $page );
1738 "Failed to determine page associated with revision {$row->rev_id}"
1743 $user = $this->actorStore->newActorFromRowFields(
1744 $row->rev_user ??
null,
1745 $row->rev_user_text ??
null,
1746 $row->rev_actor ??
null
1748 }
catch ( InvalidArgumentException $ex ) {
1749 $this->logger->warning(
'Could not load user for revision {rev_id}', [
1750 'rev_id' => $row->rev_id,
1751 'rev_actor' => $row->rev_actor ??
'null',
1752 'rev_user_text' => $row->rev_user_text ??
'null',
1753 'rev_user' => $row->rev_user ??
'null',
1756 $user = $this->actorStore->getUnknownActor();
1759 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1761 $comment = $this->commentStore->getCommentLegacy( $db,
'rev_comment', $row,
true );
1764 $slots = $this->newRevisionSlots( (
int)$row->rev_id, $row, $slots, $queryFlags, $page );
1770 function ( $revId ) use ( $queryFlags ) {
1771 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
1772 $row = $this->fetchRevisionRowFromConds(
1774 [
'rev_id' => intval( $revId ) ]
1776 if ( !$row && !( $queryFlags & self::READ_LATEST ) ) {
1778 $this->logger->info(
1779 'RevisionStoreCacheRecord refresh callback falling back to READ_LATEST.',
1782 'exception' =>
new RuntimeException(),
1785 $dbw = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
1786 $row = $this->fetchRevisionRowFromConds(
1788 [
'rev_id' => intval( $revId ) ]
1792 return [
null, null ];
1796 $this->actorStore->newActorFromRowFields(
1797 $row->rev_user ??
null,
1798 $row->rev_user_text ??
null,
1799 $row->rev_actor ??
null
1803 $page, $user, $comment, $row, $slots, $this->wikiId
1807 $page, $user, $comment, $row, $slots, $this->wikiId );
1823 private function ensureRevisionRowMatchesPage( $row,
PageIdentity $page, $context = [] ) {
1824 $revId = (int)( $row->rev_id ?? 0 );
1825 $revPageId = (int)( $row->rev_page ?? 0 );
1826 $expectedPageId = $page->
getId( $this->wikiId );
1828 if ( $revPageId && $expectedPageId && $revPageId !== $expectedPageId ) {
1830 $pageRec = $this->pageStore->getPageByName(
1833 PageStore::READ_LATEST
1835 $masterPageId = $pageRec->getId( $this->wikiId );
1836 $masterLatest = $pageRec->getLatest( $this->wikiId );
1837 if ( $revPageId === $masterPageId ) {
1838 if ( $page instanceof
Title ) {
1841 $page->resetArticleID( $masterPageId );
1847 $this->logger->info(
1848 "Encountered stale Title object",
1850 'page_id_stale' => $expectedPageId,
1851 'page_id_reloaded' => $masterPageId,
1852 'page_latest' => $masterLatest,
1854 'exception' =>
new RuntimeException(),
1858 $expectedTitle = (string)$page;
1859 if ( $page instanceof
Title ) {
1861 $page = $this->titleFactory->newFromID( $revPageId );
1872 $this->logger->error(
1873 "Encountered mismatching Title object (see T259022, T268910, T279832, T263340)",
1875 'expected_page_id' => $masterPageId,
1876 'expected_page_title' => $expectedTitle,
1877 'rev_page' => $revPageId,
1878 'rev_page_title' => (
string)$page,
1879 'page_latest' => $masterLatest,
1881 'exception' =>
new RuntimeException(),
1918 array $options = [],
1923 $archiveMode = $options[
'archive'] ??
false;
1925 if ( $archiveMode ) {
1926 $revIdField =
'ar_rev_id';
1928 $revIdField =
'rev_id';
1932 $pageIdsToFetchTitles = [];
1933 $titlesByPageKey = [];
1934 foreach ( $rows as $row ) {
1935 if ( isset( $rowsByRevId[$row->$revIdField] ) ) {
1937 'internalerror_info',
1938 "Duplicate rows in newRevisionsFromBatch, $revIdField {$row->$revIdField}"
1944 $archiveMode ? $row->ar_namespace .
':' . $row->ar_title : $row->rev_page;
1947 if ( !$archiveMode && $row->rev_page != $this->getArticleId( $page ) ) {
1948 throw new InvalidArgumentException(
1949 "Revision {$row->$revIdField} doesn't belong to page "
1950 . $this->getArticleId( $page )
1956 || $row->ar_title !== $page->
getDBkey() )
1958 throw new InvalidArgumentException(
1959 "Revision {$row->$revIdField} doesn't belong to page "
1963 } elseif ( !isset( $titlesByPageKey[ $row->_page_key ] ) ) {
1964 if ( isset( $row->page_namespace ) && isset( $row->page_title )
1967 && isset( $row->page_id ) && isset( $row->rev_page )
1968 && $row->rev_page === $row->page_id
1971 } elseif ( $archiveMode ) {
1973 $titlesByPageKey[ $row->_page_key ] =
1976 $pageIdsToFetchTitles[] = $row->rev_page;
1979 $rowsByRevId[$row->$revIdField] = $row;
1982 if ( empty( $rowsByRevId ) ) {
1983 $result->setResult(
true, [] );
1990 $pageKey = $archiveMode
1992 : $this->getArticleId( $page );
1994 $titlesByPageKey[$pageKey] = $page;
1995 } elseif ( !empty( $pageIdsToFetchTitles ) ) {
1998 Assert::invariant( !$archiveMode,
'Titles are not loaded by ID in archive mode.' );
2000 $pageIdsToFetchTitles = array_unique( $pageIdsToFetchTitles );
2001 $pageRecords = $this->pageStore
2002 ->newSelectQueryBuilder()
2003 ->wherePageIds( $pageIdsToFetchTitles )
2004 ->caller( __METHOD__ )
2005 ->fetchPageRecordArray();
2007 $titlesByPageKey = $pageRecords + $titlesByPageKey;
2011 $newRevisionRecord = [
2013 $archiveMode ?
'newRevisionFromArchiveRowAndSlots' :
'newRevisionFromRowAndSlots'
2016 if ( !isset( $options[
'slots'] ) ) {
2020 static function ( $row )
2021 use ( $queryFlags, $titlesByPageKey, $result, $newRevisionRecord, $revIdField ) {
2023 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2025 'internalerror_info',
2026 "Couldn't find title for rev {$row->$revIdField} "
2027 .
"(page key {$row->_page_key})"
2031 return $newRevisionRecord( $row,
null, $queryFlags,
2032 $titlesByPageKey[ $row->_page_key ] );
2034 $result->warning(
'internalerror_info', $e->getMessage() );
2045 'slots' => $options[
'slots'] ??
true,
2046 'blobs' => $options[
'content'] ??
false,
2049 if ( is_array( $slotRowOptions[
'slots'] )
2050 && !in_array( SlotRecord::MAIN, $slotRowOptions[
'slots'] )
2053 $slotRowOptions[
'slots'][] = SlotRecord::MAIN;
2056 $slotRowsStatus = $this->getSlotRowsForBatch( $rowsByRevId, $slotRowOptions, $queryFlags );
2058 $result->merge( $slotRowsStatus );
2059 $slotRowsByRevId = $slotRowsStatus->getValue();
2065 use ( $slotRowsByRevId, $queryFlags, $titlesByPageKey, $result,
2066 $revIdField, $newRevisionRecord
2068 if ( !isset( $slotRowsByRevId[$row->$revIdField] ) ) {
2070 'internalerror_info',
2071 "Couldn't find slots for rev {$row->$revIdField}"
2075 if ( !isset( $titlesByPageKey[$row->_page_key] ) ) {
2077 'internalerror_info',
2078 "Couldn't find title for rev {$row->$revIdField} "
2079 .
"(page key {$row->_page_key})"
2084 return $newRevisionRecord(
2087 $this->constructSlotRecords(
2089 $slotRowsByRevId[$row->$revIdField],
2091 $titlesByPageKey[$row->_page_key]
2095 $titlesByPageKey[$row->_page_key]
2098 $result->warning(
'internalerror_info', $e->getMessage() );
2131 private function getSlotRowsForBatch(
2133 array $options = [],
2139 foreach ( $rowsOrIds as $row ) {
2140 if ( is_object( $row ) ) {
2141 $revIds[] = isset( $row->ar_rev_id ) ? (int)$row->ar_rev_id : (
int)$row->rev_id;
2143 $revIds[] = (int)$row;
2149 if ( empty( $revIds ) ) {
2150 $result->setResult(
true, [] );
2155 $slotQueryInfo = $this->getSlotsQueryInfo( [
'content' ] );
2156 $revIdField = $slotQueryInfo[
'keys'][
'rev_id'];
2157 $slotQueryConds = [ $revIdField => $revIds ];
2159 if ( isset( $options[
'slots'] ) && is_array( $options[
'slots'] ) ) {
2160 if ( empty( $options[
'slots'] ) ) {
2162 $result->setResult(
true, array_fill_keys( $revIds, [] ) );
2166 $roleIdField = $slotQueryInfo[
'keys'][
'role_id'];
2167 $slotQueryConds[$roleIdField] = array_map(
2168 [ $this->slotRoleStore,
'getId' ],
2173 $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
2174 $slotRows = $db->select(
2175 $slotQueryInfo[
'tables'],
2176 $slotQueryInfo[
'fields'],
2180 $slotQueryInfo[
'joins']
2183 $slotContents =
null;
2184 if ( $options[
'blobs'] ??
false ) {
2185 $blobAddresses = [];
2186 foreach ( $slotRows as $slotRow ) {
2187 $blobAddresses[] = $slotRow->content_address;
2189 $slotContentFetchStatus = $this->blobStore
2190 ->getBlobBatch( $blobAddresses, $queryFlags );
2191 foreach ( $slotContentFetchStatus->getErrors() as $error ) {
2192 $result->warning( $error[
'message'], ...$error[
'params'] );
2194 $slotContents = $slotContentFetchStatus->getValue();
2197 $slotRowsByRevId = [];
2198 foreach ( $slotRows as $slotRow ) {
2199 if ( $slotContents ===
null ) {
2201 } elseif ( isset( $slotContents[$slotRow->content_address] ) ) {
2202 $slotRow->blob_data = $slotContents[$slotRow->content_address];
2205 'internalerror_info',
2206 "Couldn't find blob data for rev {$slotRow->slot_revision_id}"
2208 $slotRow->blob_data =
null;
2212 if ( !isset( $slotRow->role_name ) && isset( $slotRow->slot_role_id ) ) {
2213 $slotRow->role_name = $this->slotRoleStore->getName( (
int)$slotRow->slot_role_id );
2217 if ( !isset( $slotRow->model_name ) && isset( $slotRow->content_model ) ) {
2218 $slotRow->model_name = $this->contentModelStore->getName( (
int)$slotRow->content_model );
2221 $slotRowsByRevId[$slotRow->slot_revision_id][$slotRow->role_name] = $slotRow;
2224 $result->setResult(
true, $slotRowsByRevId );
2253 $result = $this->getSlotRowsForBatch(
2255 [
'slots' => $slots,
'blobs' =>
true ],
2259 if ( $result->isOK() ) {
2261 foreach ( $result->value as $revId => $rowsByRole ) {
2262 foreach ( $rowsByRole as $role => $slotRow ) {
2263 if ( is_array( $slots ) && !in_array( $role, $slots ) ) {
2266 unset( $result->value[$revId][$role] );
2270 $result->value[$revId][$role] = (object)[
2271 'blob_data' => $slotRow->blob_data,
2272 'model_name' => $slotRow->model_name,
2297 private function newRevisionFromConds(
2299 int $flags = IDBAccessObject::READ_NORMAL,
2303 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2304 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $page, $options );
2309 && !( $flags & self::READ_LATEST )
2310 && $this->loadBalancer->hasStreamingReplicaServers()
2311 && $this->loadBalancer->hasOrMadeRecentPrimaryChanges()
2313 $flags = self::READ_LATEST;
2314 $dbw = $this->getDBConnectionRef(
DB_PRIMARY );
2315 $rev = $this->loadRevisionFromConds( $dbw, $conditions, $flags, $page, $options );
2335 private function loadRevisionFromConds(
2338 int $flags = IDBAccessObject::READ_NORMAL,
2339 PageIdentity $page =
null,
2342 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags, $options );
2344 return $this->newRevisionFromRow( $row, $flags, $page );
2357 private function checkDatabaseDomain( IDatabase $db ) {
2358 $dbDomain = $db->getDomainID();
2359 $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
2360 if ( $dbDomain === $storeDomain ) {
2364 throw new MWException(
"DB connection domain '$dbDomain' does not match '$storeDomain'" );
2380 private function fetchRevisionRowFromConds(
2383 int $flags = IDBAccessObject::READ_NORMAL,
2386 $this->checkDatabaseDomain( $db );
2388 $revQuery = $this->getQueryInfo( [
'page',
'user' ] );
2389 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
2390 $options[] =
'FOR UPDATE';
2392 return $db->selectRow(
2430 $ret[
'tables'][] =
'revision';
2431 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2442 $commentQuery = $this->commentStore->getJoin(
'rev_comment' );
2443 $ret[
'tables'] = array_merge( $ret[
'tables'], $commentQuery[
'tables'] );
2444 $ret[
'fields'] = array_merge( $ret[
'fields'], $commentQuery[
'fields'] );
2445 $ret[
'joins'] = array_merge( $ret[
'joins'], $commentQuery[
'joins'] );
2447 $actorQuery = $this->actorMigration->getJoin(
'rev_user' );
2448 $ret[
'tables'] = array_merge( $ret[
'tables'], $actorQuery[
'tables'] );
2449 $ret[
'fields'] = array_merge( $ret[
'fields'], $actorQuery[
'fields'] );
2450 $ret[
'joins'] = array_merge( $ret[
'joins'], $actorQuery[
'joins'] );
2452 if ( in_array(
'page', $options,
true ) ) {
2453 $ret[
'tables'][] =
'page';
2454 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2462 $ret[
'joins'][
'page'] = [
'JOIN', [
'page_id = rev_page' ] ];
2465 if ( in_array(
'user', $options,
true ) ) {
2466 $ret[
'tables'][] =
'user';
2467 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2470 $u = $actorQuery[
'fields'][
'rev_user'];
2471 $ret[
'joins'][
'user'] = [
'LEFT JOIN', [
"$u != 0",
"user_id = $u" ] ];
2474 if ( in_array(
'text', $options,
true ) ) {
2475 throw new InvalidArgumentException(
2476 'The `text` option is no longer supported in MediaWiki 1.35 and later.'
2512 $ret[
'keys'][
'rev_id'] =
'slot_revision_id';
2513 $ret[
'keys'][
'role_id'] =
'slot_role_id';
2515 $ret[
'tables'][] =
'slots';
2516 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2523 if ( in_array(
'role', $options,
true ) ) {
2526 $ret[
'tables'][] =
'slot_roles';
2527 $ret[
'joins'][
'slot_roles'] = [
'LEFT JOIN', [
'slot_role_id = role_id' ] ];
2528 $ret[
'fields'][] =
'role_name';
2531 if ( in_array(
'content', $options,
true ) ) {
2532 $ret[
'keys'][
'model_id'] =
'content_model';
2534 $ret[
'tables'][] =
'content';
2535 $ret[
'fields'] = array_merge( $ret[
'fields'], [
2541 $ret[
'joins'][
'content'] = [
'JOIN', [
'slot_content_id = content_id' ] ];
2543 if ( in_array(
'model', $options,
true ) ) {
2546 $ret[
'tables'][] =
'content_models';
2547 $ret[
'joins'][
'content_models'] = [
'LEFT JOIN', [
'content_model = model_id' ] ];
2548 $ret[
'fields'][] =
'model_name';
2565 if ( !( $row instanceof stdClass ) ) {
2568 $queryInfo = $table ===
'archive' ? $this->getArchiveQueryInfo() : $this->getQueryInfo();
2569 foreach ( $queryInfo[
'fields'] as $alias => $field ) {
2570 $name = is_numeric( $alias ) ? $field : $alias;
2571 if ( !property_exists( $row, $name ) ) {
2597 $commentQuery = $this->commentStore->getJoin(
'ar_comment' );
2601 'archive_actor' =>
'actor'
2602 ] + $commentQuery[
'tables'],
2616 'ar_user' =>
'archive_actor.actor_user',
2617 'ar_user_text' =>
'archive_actor.actor_name',
2618 ] + $commentQuery[
'fields'],
2620 'archive_actor' => [
'JOIN',
'actor_id=ar_actor' ]
2621 ] + $commentQuery[
'joins'],
2645 [
'rev_id',
'rev_len' ],
2646 [
'rev_id' => $revIds ],
2650 foreach (
$res as $row ) {
2651 $revLens[$row->rev_id] = intval( $row->rev_len );
2665 private function getRelativeRevision(
RevisionRecord $rev, $flags, $dir ) {
2666 $op = $dir ===
'next' ?
'>' :
'<';
2667 $sort = $dir ===
'next' ?
'ASC' :
'DESC';
2669 $revisionIdValue = $rev->
getId( $this->wikiId );
2671 if ( !$revisionIdValue || !$rev->
getPageId( $this->wikiId ) ) {
2676 if ( $rev instanceof RevisionArchiveRecord ) {
2682 $db = $this->getDBConnectionRef( $dbType, [
'contributions' ] );
2684 $ts = $rev->
getTimestamp() ?? $this->getTimestampFromId( $revisionIdValue, $flags );
2685 if ( $ts ===
false ) {
2687 $ts = $db->selectField(
'archive',
'ar_timestamp',
2688 [
'ar_rev_id' => $revisionIdValue ], __METHOD__ );
2689 if ( $ts ===
false ) {
2695 $revId = $db->selectField(
'revision',
'rev_id',
2697 'rev_page' => $rev->
getPageId( $this->wikiId ),
2698 $db->buildComparison( $op, [
2699 'rev_timestamp' => $db->timestamp( $ts ),
2700 'rev_id' => $revisionIdValue,
2705 'ORDER BY' => [
"rev_timestamp $sort",
"rev_id $sort" ],
2706 'IGNORE INDEX' =>
'rev_timestamp',
2710 if ( $revId ===
false ) {
2714 return $this->getRevisionById( intval( $revId ), $flags );
2732 return $this->getRelativeRevision( $rev, $flags,
'prev' );
2747 return $this->getRelativeRevision( $rev, $flags,
'next' );
2762 $this->checkDatabaseDomain( $db );
2764 if ( $rev->
getPageId( $this->wikiId ) ===
null ) {
2767 # Use page_latest if ID is not given
2768 if ( !$rev->
getId( $this->wikiId ) ) {
2770 'page',
'page_latest',
2771 [
'page_id' => $rev->
getPageId( $this->wikiId ) ],
2776 'revision',
'rev_id',
2777 [
'rev_page' => $rev->
getPageId( $this->wikiId ),
'rev_id < ' . $rev->
getId( $this->wikiId ) ],
2779 [
'ORDER BY' =>
'rev_id DESC' ]
2782 return intval( $prevId );
2798 if ( $id instanceof
Title ) {
2801 $flags = func_num_args() > 2 ? func_get_arg( 2 ) : 0;
2809 if ( $id ===
null || $id <= 0 ) {
2813 $db = $this->getDBConnectionRefForQueryFlags( $flags );
2816 $db->
selectField(
'revision',
'rev_timestamp', [
'rev_id' => $id ], __METHOD__ );
2818 return ( $timestamp !==
false ) ? MWTimestamp::convert( TS_MW, $timestamp ) :
false;
2831 $this->checkDatabaseDomain( $db );
2834 [
'revCount' =>
'COUNT(*)' ],
2835 [
'rev_page' => $id ],
2839 return intval( $row->revCount );
2854 $id = $this->getArticleId( $page );
2856 return $this->countRevisionsByPageId( $db, $id );
2880 $this->checkDatabaseDomain( $db );
2890 'rev_user' =>
$revQuery[
'fields'][
'rev_user'],
2893 'rev_page' => $pageId,
2897 [
'ORDER BY' =>
'rev_timestamp ASC',
'LIMIT' => 50 ],
2900 foreach (
$res as $row ) {
2901 if ( $row->rev_user != $userId ) {
2922 $db = $this->getDBConnectionRef(
DB_REPLICA );
2923 $revIdPassed = $revId;
2924 $pageId = $this->getArticleId( $page );
2930 if ( $page instanceof
Title ) {
2931 $revId = $page->getLatestRevID();
2933 $pageRecord = $this->pageStore->getPageByReference( $page );
2934 if ( $pageRecord ) {
2935 $revId = $pageRecord->getLatest( $this->getWikiId() );
2941 $this->logger->warning(
2942 'No latest revision known for page {page} even though it exists with page ID {page_id}', [
2944 'page_id' => $pageId,
2945 'wiki_id' => $this->getWikiId() ?:
'local',
2954 $row = $this->cache->getWithSetCallback(
2956 $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
2957 WANObjectCache::TTL_WEEK,
2958 function ( $curValue, &$ttl, array &$setOpts ) use (
2959 $db, $revId, &$fromCache
2961 $setOpts += Database::getCacheSetOptions( $db );
2962 $row = $this->fetchRevisionRowFromConds( $db, [
'rev_id' => intval( $revId ) ] );
2972 $title = $this->ensureRevisionRowMatchesPage( $row, $page, [
2973 'from_cache_flag' => $fromCache,
2974 'page_id_initial' => $pageId,
2975 'rev_id_used' => $revId,
2976 'rev_id_requested' => $revIdPassed,
2979 return $this->newRevisionFromRow( $row, 0,
$title, $fromCache );
2995 int $flags = IDBAccessObject::READ_NORMAL
3001 return $this->newRevisionFromConds(
3009 'ORDER BY' => [
'rev_timestamp ASC',
'rev_id ASC' ],
3010 'IGNORE INDEX' => [
'revision' =>
'rev_timestamp' ],
3026 private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
3027 return $this->cache->makeGlobalKey(
3028 self::ROW_CACHE_KEY,
3042 private function assertRevisionParameter( $paramName, $pageId, RevisionRecord $rev =
null ) {
3044 if ( $rev->
getId( $this->wikiId ) ===
null ) {
3045 throw new InvalidArgumentException(
"Unsaved {$paramName} revision passed" );
3047 if ( $rev->
getPageId( $this->wikiId ) !== $pageId ) {
3048 throw new InvalidArgumentException(
3049 "Revision {$rev->getId( $this->wikiId )} doesn't belong to page {$pageId}"
3069 private function getRevisionLimitConditions(
3071 RevisionRecord $old =
null,
3072 RevisionRecord $new =
null,
3075 $options = (array)$options;
3076 if ( in_array( self::INCLUDE_OLD, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) {
3081 if ( in_array( self::INCLUDE_NEW, $options ) || in_array( self::INCLUDE_BOTH, $options ) ) {
3089 $conds[] =
$dbr->buildComparison( $oldCmp, [
3090 'rev_timestamp' =>
$dbr->timestamp( $old->getTimestamp() ),
3091 'rev_id' => $old->getId( $this->wikiId ),
3095 $conds[] =
$dbr->buildComparison( $newCmp, [
3096 'rev_timestamp' =>
$dbr->timestamp( $new->getTimestamp() ),
3097 'rev_id' => $new->getId( $this->wikiId ),
3135 ?
string $order =
null,
3136 int $flags = IDBAccessObject::READ_NORMAL
3138 $this->assertRevisionParameter(
'old', $pageId, $old );
3139 $this->assertRevisionParameter(
'new', $pageId, $new );
3141 $options = (array)$options;
3142 $includeOld = in_array( self::INCLUDE_OLD, $options ) ||
3143 in_array( self::INCLUDE_BOTH, $options );
3144 $includeNew = in_array( self::INCLUDE_NEW, $options ) ||
3145 in_array( self::INCLUDE_BOTH, $options );
3151 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3152 return $includeOld || $includeNew ? [ $new->getId( $this->wikiId ) ] : [];
3155 $db = $this->getDBConnectionRefForQueryFlags( $flags );
3156 $conds = array_merge(
3158 'rev_page' => $pageId,
3159 $db->bitAnd(
'rev_deleted', RevisionRecord::DELETED_TEXT ) .
' = 0'
3161 $this->getRevisionLimitConditions( $db, $old, $new, $options )
3165 if ( $order !==
null ) {
3166 $queryOptions[
'ORDER BY'] = [
"rev_timestamp $order",
"rev_id $order" ];
3168 if ( $max !==
null ) {
3169 $queryOptions[
'LIMIT'] = $max + 1;
3172 $values = $db->selectFieldValues(
3179 return array_map(
'intval', $values );
3211 $this->assertRevisionParameter(
'old', $pageId, $old );
3212 $this->assertRevisionParameter(
'new', $pageId, $new );
3213 $options = (array)$options;
3219 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3220 if ( empty( $options ) ) {
3222 } elseif ( $performer ) {
3223 return [ $new->getUser( RevisionRecord::FOR_THIS_USER, $performer ) ];
3225 return [ $new->getUser() ];
3230 $conds = array_merge(
3232 'rev_page' => $pageId,
3233 $dbr->bitAnd(
'rev_deleted', RevisionRecord::DELETED_USER ) .
" = 0"
3235 $this->getRevisionLimitConditions(
$dbr, $old, $new, $options )
3238 $queryOpts = [
'DISTINCT' ];
3239 if ( $max !==
null ) {
3240 $queryOpts[
'LIMIT'] = $max + 1;
3243 $actorQuery = $this->actorMigration->getJoin(
'rev_user' );
3244 return array_map(
function ( $row ) {
3245 return $this->actorStore->newActorFromRowFields(
3247 $row->rev_user_text,
3250 }, iterator_to_array(
$dbr->select(
3251 array_merge( [
'revision' ], $actorQuery[
'tables'] ),
3252 $actorQuery[
'fields'],
3255 $actorQuery[
'joins']
3290 return count( $this->getAuthorsBetween( $pageId, $old, $new, $performer, $max, $options ) );
3320 $this->assertRevisionParameter(
'old', $pageId, $old );
3321 $this->assertRevisionParameter(
'new', $pageId, $new );
3327 if ( $old && $new && $new->getId( $this->wikiId ) === $old->getId( $this->wikiId ) ) {
3332 $conds = array_merge(
3334 'rev_page' => $pageId,
3335 $dbr->bitAnd(
'rev_deleted', RevisionRecord::DELETED_TEXT ) .
" = 0"
3337 $this->getRevisionLimitConditions(
$dbr, $old, $new, $options )
3339 if ( $max !==
null ) {
3340 return $dbr->selectRowCount(
'revision',
'1',
3343 [
'LIMIT' => $max + 1 ]
3346 return (
int)
$dbr->selectField(
'revision',
'count(*)', $conds, __METHOD__ );
3366 $db = $this->getDBConnectionRef(
DB_REPLICA );
3368 $subquery = $db->buildSelectSubquery(
3371 [
'rev_page' => $revision->
getPageId( $this->wikiId ) ],
3375 'rev_timestamp DESC',
3379 'LIMIT' => $searchLimit,
3387 $revisionRow = $db->selectRow(
3388 [
'recent_revs' => $subquery ],
3390 [
'rev_sha1' => $revision->
getSha1() ],
3394 return $revisionRow ? $this->newRevisionFromRow( $revisionRow ) : null;
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Class representing a cache/ephemeral data store.
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)
Content object implementation representing unknown content.
Library for creating and parsing MW-style timestamps.
Exception thrown when an unregistered content model is requested.
Immutable value object representing a page identity.
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.
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
static newFromRow( $row)
Make a Title object from a DB row.
Multi-datacenter aware caching interface.
Base interface for representing page content.
Interface for database access objects.
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.
trait LegacyArticleIdAccess
Convenience trait for conversion to PageIdentity.