MediaWiki REL1_31
RevisionStore.php
Go to the documentation of this file.
1<?php
27namespace MediaWiki\Storage;
28
37use InvalidArgumentException;
38use IP;
39use LogicException;
46use Psr\Log\LoggerAwareInterface;
47use Psr\Log\LoggerInterface;
48use Psr\Log\NullLogger;
50use stdClass;
52use User;
54use Wikimedia\Assert\Assert;
59
69 implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
70
74 private $blobStore;
75
79 private $wikiId;
80
84 private $contentHandlerUseDB = true;
85
90
94 private $cache;
95
100
105
109 private $logger;
110
121 public function __construct(
127 $wikiId = false
128 ) {
129 Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
130
131 $this->loadBalancer = $loadBalancer;
132 $this->blobStore = $blobStore;
133 $this->cache = $cache;
134 $this->commentStore = $commentStore;
135 $this->actorMigration = $actorMigration;
136 $this->wikiId = $wikiId;
137 $this->logger = new NullLogger();
138 }
139
140 public function setLogger( LoggerInterface $logger ) {
141 $this->logger = $logger;
142 }
143
147 public function isReadOnly() {
148 return $this->blobStore->isReadOnly();
149 }
150
154 public function getContentHandlerUseDB() {
156 }
157
162 $this->contentHandlerUseDB = $contentHandlerUseDB;
163 }
164
168 private function getDBLoadBalancer() {
169 return $this->loadBalancer;
170 }
171
177 private function getDBConnection( $mode ) {
178 $lb = $this->getDBLoadBalancer();
179 return $lb->getConnection( $mode, [], $this->wikiId );
180 }
181
185 private function releaseDBConnection( IDatabase $connection ) {
186 $lb = $this->getDBLoadBalancer();
187 $lb->reuseConnection( $connection );
188 }
189
195 private function getDBConnectionRef( $mode ) {
196 $lb = $this->getDBLoadBalancer();
197 return $lb->getConnectionRef( $mode, [], $this->wikiId );
198 }
199
214 public function getTitle( $pageId, $revId, $queryFlags = self::READ_NORMAL ) {
215 if ( !$pageId && !$revId ) {
216 throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
217 }
218
219 // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
220 // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
221 if ( DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST_IMMUTABLE ) ) {
222 $queryFlags = self::READ_NORMAL;
223 }
224
225 $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->wikiId === false );
226 list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
227 $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
228
229 // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
230 if ( $canUseTitleNewFromId ) {
231 // TODO: better foreign title handling (introduce TitleFactory)
232 $title = Title::newFromID( $pageId, $titleFlags );
233 if ( $title ) {
234 return $title;
235 }
236 }
237
238 // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
239 $canUseRevId = ( $revId !== null && $revId > 0 );
240
241 if ( $canUseRevId ) {
242 $dbr = $this->getDBConnectionRef( $dbMode );
243 // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
244 $row = $dbr->selectRow(
245 [ 'revision', 'page' ],
246 [
247 'page_namespace',
248 'page_title',
249 'page_id',
250 'page_latest',
251 'page_is_redirect',
252 'page_len',
253 ],
254 [ 'rev_id' => $revId ],
255 __METHOD__,
256 $dbOptions,
257 [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
258 );
259 if ( $row ) {
260 // TODO: better foreign title handling (introduce TitleFactory)
261 return Title::newFromRow( $row );
262 }
263 }
264
265 // If we still don't have a title, fallback to master if that wasn't already happening.
266 if ( $dbMode !== DB_MASTER ) {
267 $title = $this->getTitle( $pageId, $revId, self::READ_LATEST );
268 if ( $title ) {
269 $this->logger->info(
270 __METHOD__ . ' fell back to READ_LATEST and got a Title.',
271 [ 'trace' => wfBacktrace() ]
272 );
273 return $title;
274 }
275 }
276
277 throw new RevisionAccessException(
278 "Could not determine title for page ID $pageId and revision ID $revId"
279 );
280 }
281
289 private function failOnNull( $value, $name ) {
290 if ( $value === null ) {
292 "$name must not be " . var_export( $value, true ) . "!"
293 );
294 }
295
296 return $value;
297 }
298
306 private function failOnEmpty( $value, $name ) {
307 if ( $value === null || $value === 0 || $value === '' ) {
309 "$name must not be " . var_export( $value, true ) . "!"
310 );
311 }
312
313 return $value;
314 }
315
329 // TODO: pass in a DBTransactionContext instead of a database connection.
330 $this->checkDatabaseWikiId( $dbw );
331
332 if ( !$rev->getSlotRoles() ) {
333 throw new InvalidArgumentException( 'At least one slot needs to be defined!' );
334 }
335
336 if ( $rev->getSlotRoles() !== [ 'main' ] ) {
337 throw new InvalidArgumentException( 'Only the main slot is supported for now!' );
338 }
339
340 // TODO: we shouldn't need an actual Title here.
341 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
342 $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
343
344 $parentId = $rev->getParentId() === null
345 ? $this->getPreviousRevisionId( $dbw, $rev )
346 : $rev->getParentId();
347
348 // Record the text (or external storage URL) to the blob store
349 $slot = $rev->getSlot( 'main', RevisionRecord::RAW );
350
351 $size = $this->failOnNull( $rev->getSize(), 'size field' );
352 $sha1 = $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
353
354 if ( !$slot->hasAddress() ) {
355 $content = $slot->getContent();
356 $format = $content->getDefaultFormat();
357 $model = $content->getModel();
358
359 $this->checkContentModel( $content, $title );
360
361 $data = $content->serialize( $format );
362
363 // Hints allow the blob store to optimize by "leaking" application level information to it.
364 // TODO: with the new MCR storage schema, we rev_id have this before storing the blobs.
365 // When we have it, add rev_id as a hint. Can be used with rev_parent_id for
366 // differential storage or compression of subsequent revisions.
367 $blobHints = [
368 BlobStore::DESIGNATION_HINT => 'page-content', // BlobStore may be used for other things too.
369 BlobStore::PAGE_HINT => $pageId,
370 BlobStore::ROLE_HINT => $slot->getRole(),
371 BlobStore::PARENT_HINT => $parentId,
372 BlobStore::SHA1_HINT => $slot->getSha1(),
373 BlobStore::MODEL_HINT => $model,
374 BlobStore::FORMAT_HINT => $format,
375 ];
376
377 $blobAddress = $this->blobStore->storeBlob( $data, $blobHints );
378 } else {
379 $blobAddress = $slot->getAddress();
380 $model = $slot->getModel();
381 $format = $slot->getFormat();
382 }
383
384 $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
385
386 if ( !$textId ) {
387 throw new LogicException(
388 'Blob address not supported in 1.29 database schema: ' . $blobAddress
389 );
390 }
391
392 // getTextIdFromAddress() is free to insert something into the text table, so $textId
393 // may be a new value, not anything already contained in $blobAddress.
394 $blobAddress = 'tt:' . $textId;
395
396 $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
397 $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
398 $timestamp = $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
399
400 // Checks.
401 $this->failOnNull( $user->getId(), 'user field' );
402 $this->failOnEmpty( $user->getName(), 'user_text field' );
403
404 # Record the edit in revisions
405 $row = [
406 'rev_page' => $pageId,
407 'rev_parent_id' => $parentId,
408 'rev_text_id' => $textId,
409 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
410 'rev_timestamp' => $dbw->timestamp( $timestamp ),
411 'rev_deleted' => $rev->getVisibility(),
412 'rev_len' => $size,
413 'rev_sha1' => $sha1,
414 ];
415
416 if ( $rev->getId() !== null ) {
417 // Needed to restore revisions with their original ID
418 $row['rev_id'] = $rev->getId();
419 }
420
421 list( $commentFields, $commentCallback ) =
422 $this->commentStore->insertWithTempTable( $dbw, 'rev_comment', $comment );
423 $row += $commentFields;
424
425 list( $actorFields, $actorCallback ) =
426 $this->actorMigration->getInsertValuesWithTempTable( $dbw, 'rev_user', $user );
427 $row += $actorFields;
428
429 if ( $this->contentHandlerUseDB ) {
430 // MCR migration note: rev_content_model and rev_content_format will go away
431
432 $defaultModel = ContentHandler::getDefaultModelFor( $title );
433 $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
434
435 $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
436 $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
437 }
438
439 $dbw->insert( 'revision', $row, __METHOD__ );
440
441 if ( !isset( $row['rev_id'] ) ) {
442 // only if auto-increment was used
443 $row['rev_id'] = intval( $dbw->insertId() );
444 }
445 $commentCallback( $row['rev_id'] );
446 $actorCallback( $row['rev_id'], $row );
447
448 // Insert IP revision into ip_changes for use when querying for a range.
449 if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
450 $ipcRow = [
451 'ipc_rev_id' => $row['rev_id'],
452 'ipc_rev_timestamp' => $row['rev_timestamp'],
453 'ipc_hex' => IP::toHex( $user->getName() ),
454 ];
455 $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
456 }
457
458 $newSlot = SlotRecord::newSaved( $row['rev_id'], $textId, $blobAddress, $slot );
459 $slots = new RevisionSlots( [ 'main' => $newSlot ] );
460
462 $title,
463 $user,
464 $comment,
465 (object)$row,
466 $slots,
467 $this->wikiId
468 );
469
470 $newSlot = $rev->getSlot( 'main', RevisionRecord::RAW );
471
472 // sanity checks
473 Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
474 Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
475 Assert::postcondition(
476 $rev->getComment( RevisionRecord::RAW ) !== null,
477 'revision must have a comment'
478 );
479 Assert::postcondition(
480 $rev->getUser( RevisionRecord::RAW ) !== null,
481 'revision must have a user'
482 );
483
484 Assert::postcondition( $newSlot !== null, 'revision must have a main slot' );
485 Assert::postcondition(
486 $newSlot->getAddress() !== null,
487 'main slot must have an addess'
488 );
489
490 Hooks::run( 'RevisionRecordInserted', [ $rev ] );
491
492 return $rev;
493 }
494
504 private function checkContentModel( Content $content, Title $title ) {
505 // Note: may return null for revisions that have not yet been inserted
506
507 $model = $content->getModel();
508 $format = $content->getDefaultFormat();
509 $handler = $content->getContentHandler();
510
511 $name = "$title";
512
513 if ( !$handler->isSupportedFormat( $format ) ) {
514 throw new MWException( "Can't use format $format with content model $model on $name" );
515 }
516
517 if ( !$this->contentHandlerUseDB ) {
518 // if $wgContentHandlerUseDB is not set,
519 // all revisions must use the default content model and format.
520
521 $defaultModel = ContentHandler::getDefaultModelFor( $title );
522 $defaultHandler = ContentHandler::getForModelID( $defaultModel );
523 $defaultFormat = $defaultHandler->getDefaultFormat();
524
525 if ( $model != $defaultModel ) {
526 throw new MWException( "Can't save non-default content model with "
527 . "\$wgContentHandlerUseDB disabled: model is $model, "
528 . "default for $name is $defaultModel"
529 );
530 }
531
532 if ( $format != $defaultFormat ) {
533 throw new MWException( "Can't use non-default content format with "
534 . "\$wgContentHandlerUseDB disabled: format is $format, "
535 . "default for $name is $defaultFormat"
536 );
537 }
538 }
539
540 if ( !$content->isValid() ) {
541 throw new MWException(
542 "New content for $name is not valid! Content model is $model"
543 );
544 }
545 }
546
567 public function newNullRevision(
568 IDatabase $dbw,
569 Title $title,
570 CommentStoreComment $comment,
571 $minor,
572 User $user
573 ) {
574 $this->checkDatabaseWikiId( $dbw );
575
576 $fields = [ 'page_latest', 'page_namespace', 'page_title',
577 'rev_id', 'rev_text_id', 'rev_len', 'rev_sha1' ];
578
579 if ( $this->contentHandlerUseDB ) {
580 $fields[] = 'rev_content_model';
581 $fields[] = 'rev_content_format';
582 }
583
584 $current = $dbw->selectRow(
585 [ 'page', 'revision' ],
586 $fields,
587 [
588 'page_id' => $title->getArticleID(),
589 'page_latest=rev_id',
590 ],
591 __METHOD__,
592 [ 'FOR UPDATE' ] // T51581
593 );
594
595 if ( $current ) {
596 $fields = [
597 'page' => $title->getArticleID(),
598 'user_text' => $user->getName(),
599 'user' => $user->getId(),
600 'actor' => $user->getActorId(),
601 'comment' => $comment,
602 'minor_edit' => $minor,
603 'text_id' => $current->rev_text_id,
604 'parent_id' => $current->page_latest,
605 'slot_origin' => $current->page_latest,
606 'len' => $current->rev_len,
607 'sha1' => $current->rev_sha1
608 ];
609
610 if ( $this->contentHandlerUseDB ) {
611 $fields['content_model'] = $current->rev_content_model;
612 $fields['content_format'] = $current->rev_content_format;
613 }
614
615 $fields['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
616
617 $mainSlot = $this->emulateMainSlot_1_29( $fields, self::READ_LATEST, $title );
618 $revision = new MutableRevisionRecord( $title, $this->wikiId );
619 $this->initializeMutableRevisionFromArray( $revision, $fields );
620 $revision->setSlot( $mainSlot );
621 } else {
622 $revision = null;
623 }
624
625 return $revision;
626 }
627
638 $rc = $this->getRecentChange( $rev );
639 if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED ) {
640 return $rc->getAttribute( 'rc_id' );
641 } else {
642 return 0;
643 }
644 }
645
659 public function getRecentChange( RevisionRecord $rev, $flags = 0 ) {
660 $dbr = $this->getDBConnection( DB_REPLICA );
661
662 list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
663
664 $userIdentity = $rev->getUser( RevisionRecord::RAW );
665
666 if ( !$userIdentity ) {
667 // If the revision has no user identity, chances are it never went
668 // into the database, and doesn't have an RC entry.
669 return null;
670 }
671
672 // TODO: Select by rc_this_oldid alone - but as of Nov 2017, there is no index on that!
673 $actorWhere = $this->actorMigration->getWhere( $dbr, 'rc_user', $rev->getUser(), false );
675 [
676 $actorWhere['conds'],
677 'rc_timestamp' => $dbr->timestamp( $rev->getTimestamp() ),
678 'rc_this_oldid' => $rev->getId()
679 ],
680 __METHOD__,
681 $dbType
682 );
683
684 $this->releaseDBConnection( $dbr );
685
686 // XXX: cache this locally? Glue it to the RevisionRecord?
687 return $rc;
688 }
689
697 private static function mapArchiveFields( $archiveRow ) {
698 $fieldMap = [
699 // keep with ar prefix:
700 'ar_id' => 'ar_id',
701
702 // not the same suffix:
703 'ar_page_id' => 'rev_page',
704 'ar_rev_id' => 'rev_id',
705
706 // same suffix:
707 'ar_text_id' => 'rev_text_id',
708 'ar_timestamp' => 'rev_timestamp',
709 'ar_user_text' => 'rev_user_text',
710 'ar_user' => 'rev_user',
711 'ar_actor' => 'rev_actor',
712 'ar_minor_edit' => 'rev_minor_edit',
713 'ar_deleted' => 'rev_deleted',
714 'ar_len' => 'rev_len',
715 'ar_parent_id' => 'rev_parent_id',
716 'ar_sha1' => 'rev_sha1',
717 'ar_comment' => 'rev_comment',
718 'ar_comment_cid' => 'rev_comment_cid',
719 'ar_comment_id' => 'rev_comment_id',
720 'ar_comment_text' => 'rev_comment_text',
721 'ar_comment_data' => 'rev_comment_data',
722 'ar_comment_old' => 'rev_comment_old',
723 'ar_content_format' => 'rev_content_format',
724 'ar_content_model' => 'rev_content_model',
725 ];
726
727 $revRow = new stdClass();
728 foreach ( $fieldMap as $arKey => $revKey ) {
729 if ( property_exists( $archiveRow, $arKey ) ) {
730 $revRow->$revKey = $archiveRow->$arKey;
731 }
732 }
733
734 return $revRow;
735 }
736
747 private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
748 $mainSlotRow = new stdClass();
749 $mainSlotRow->role_name = 'main';
750 $mainSlotRow->model_name = null;
751 $mainSlotRow->slot_revision_id = null;
752 $mainSlotRow->content_address = null;
753 $mainSlotRow->slot_content_id = null;
754
755 $content = null;
756 $blobData = null;
757 $blobFlags = null;
758
759 if ( is_object( $row ) ) {
760 // archive row
761 if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
762 $row = $this->mapArchiveFields( $row );
763 }
764
765 if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
766 $mainSlotRow->slot_content_id = $row->rev_text_id;
767 $mainSlotRow->content_address = 'tt:' . $row->rev_text_id;
768 }
769
770 // This is used by null-revisions
771 $mainSlotRow->slot_origin = isset( $row->slot_origin )
772 ? intval( $row->slot_origin )
773 : null;
774
775 if ( isset( $row->old_text ) ) {
776 // this happens when the text-table gets joined directly, in the pre-1.30 schema
777 $blobData = isset( $row->old_text ) ? strval( $row->old_text ) : null;
778 // Check against selects that might have not included old_flags
779 if ( !property_exists( $row, 'old_flags' ) ) {
780 throw new InvalidArgumentException( 'old_flags was not set in $row' );
781 }
782 $blobFlags = ( $row->old_flags === null ) ? '' : $row->old_flags;
783 }
784
785 $mainSlotRow->slot_revision_id = intval( $row->rev_id );
786
787 $mainSlotRow->content_size = isset( $row->rev_len ) ? intval( $row->rev_len ) : null;
788 $mainSlotRow->content_sha1 = isset( $row->rev_sha1 ) ? strval( $row->rev_sha1 ) : null;
789 $mainSlotRow->model_name = isset( $row->rev_content_model )
790 ? strval( $row->rev_content_model )
791 : null;
792 // XXX: in the future, we'll probably always use the default format, and drop content_format
793 $mainSlotRow->format_name = isset( $row->rev_content_format )
794 ? strval( $row->rev_content_format )
795 : null;
796 } elseif ( is_array( $row ) ) {
797 $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
798
799 $mainSlotRow->slot_content_id = isset( $row['text_id'] )
800 ? intval( $row['text_id'] )
801 : null;
802 $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
803 ? intval( $row['slot_origin'] )
804 : null;
805 $mainSlotRow->content_address = isset( $row['text_id'] )
806 ? 'tt:' . intval( $row['text_id'] )
807 : null;
808 $mainSlotRow->content_size = isset( $row['len'] ) ? intval( $row['len'] ) : null;
809 $mainSlotRow->content_sha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
810
811 $mainSlotRow->model_name = isset( $row['content_model'] )
812 ? strval( $row['content_model'] ) : null; // XXX: must be a string!
813 // XXX: in the future, we'll probably always use the default format, and drop content_format
814 $mainSlotRow->format_name = isset( $row['content_format'] )
815 ? strval( $row['content_format'] ) : null;
816 $blobData = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
817 // XXX: If the flags field is not set then $blobFlags should be null so that no
818 // decoding will happen. An empty string will result in default decodings.
819 $blobFlags = isset( $row['flags'] ) ? trim( strval( $row['flags'] ) ) : null;
820
821 // if we have a Content object, override mText and mContentModel
822 if ( !empty( $row['content'] ) ) {
823 if ( !( $row['content'] instanceof Content ) ) {
824 throw new MWException( 'content field must contain a Content object.' );
825 }
826
828 $content = $row['content'];
829 $handler = $content->getContentHandler();
830
831 $mainSlotRow->model_name = $content->getModel();
832
833 // XXX: in the future, we'll probably always use the default format.
834 if ( $mainSlotRow->format_name === null ) {
835 $mainSlotRow->format_name = $handler->getDefaultFormat();
836 }
837 }
838 } else {
839 throw new MWException( 'Revision constructor passed invalid row format.' );
840 }
841
842 // With the old schema, the content changes with every revision,
843 // except for null-revisions.
844 if ( !isset( $mainSlotRow->slot_origin ) ) {
845 $mainSlotRow->slot_origin = $mainSlotRow->slot_revision_id;
846 }
847
848 if ( $mainSlotRow->model_name === null ) {
849 $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) {
850 // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget!
851 // TODO: MCR: deprecate $title->getModel().
852 return ContentHandler::getDefaultModelFor( $title );
853 };
854 }
855
856 if ( !$content ) {
857 $content = function ( SlotRecord $slot )
858 use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
859 {
860 return $this->loadSlotContent(
861 $slot,
862 $blobData,
863 $blobFlags,
864 $mainSlotRow->format_name,
865 $queryFlags
866 );
867 };
868 }
869
870 $mainSlotRow->slot_id = $mainSlotRow->slot_revision_id;
871 return new SlotRecord( $mainSlotRow, $content );
872 }
873
893 private function loadSlotContent(
894 SlotRecord $slot,
895 $blobData = null,
896 $blobFlags = null,
897 $blobFormat = null,
898 $queryFlags = 0
899 ) {
900 if ( $blobData !== null ) {
901 Assert::parameterType( 'string', $blobData, '$blobData' );
902 Assert::parameterType( 'string|null', $blobFlags, '$blobFlags' );
903
904 $cacheKey = $slot->hasAddress() ? $slot->getAddress() : null;
905
906 if ( $blobFlags === null ) {
907 // No blob flags, so use the blob verbatim.
908 $data = $blobData;
909 } else {
910 $data = $this->blobStore->expandBlob( $blobData, $blobFlags, $cacheKey );
911 if ( $data === false ) {
912 throw new RevisionAccessException(
913 "Failed to expand blob data using flags $blobFlags (key: $cacheKey)"
914 );
915 }
916 }
917
918 } else {
919 $address = $slot->getAddress();
920 try {
921 $data = $this->blobStore->getBlob( $address, $queryFlags );
922 } catch ( BlobAccessException $e ) {
923 throw new RevisionAccessException(
924 "Failed to load data blob from $address: " . $e->getMessage(), 0, $e
925 );
926 }
927 }
928
929 // Unserialize content
930 $handler = ContentHandler::getForModelID( $slot->getModel() );
931
932 $content = $handler->unserializeContent( $data, $blobFormat );
933 return $content;
934 }
935
950 public function getRevisionById( $id, $flags = 0 ) {
951 return $this->newRevisionFromConds( [ 'rev_id' => intval( $id ) ], $flags );
952 }
953
970 public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 ) {
971 $conds = [
972 'page_namespace' => $linkTarget->getNamespace(),
973 'page_title' => $linkTarget->getDBkey()
974 ];
975 if ( $revId ) {
976 // Use the specified revision ID.
977 // Note that we use newRevisionFromConds here because we want to retry
978 // and fall back to master if the page is not found on a replica.
979 // Since the caller supplied a revision ID, we are pretty sure the revision is
980 // supposed to exist, so we should try hard to find it.
981 $conds['rev_id'] = $revId;
982 return $this->newRevisionFromConds( $conds, $flags );
983 } else {
984 // Use a join to get the latest revision.
985 // Note that we don't use newRevisionFromConds here because we don't want to retry
986 // and fall back to master. The assumption is that we only want to force the fallback
987 // if we are quite sure the revision exists because the caller supplied a revision ID.
988 // If the page isn't found at all on a replica, it probably simply does not exist.
989 $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
990
991 $conds[] = 'rev_id=page_latest';
992 $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
993
994 $this->releaseDBConnection( $db );
995 return $rev;
996 }
997 }
998
1015 public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 ) {
1016 $conds = [ 'page_id' => $pageId ];
1017 if ( $revId ) {
1018 // Use the specified revision ID.
1019 // Note that we use newRevisionFromConds here because we want to retry
1020 // and fall back to master if the page is not found on a replica.
1021 // Since the caller supplied a revision ID, we are pretty sure the revision is
1022 // supposed to exist, so we should try hard to find it.
1023 $conds['rev_id'] = $revId;
1024 return $this->newRevisionFromConds( $conds, $flags );
1025 } else {
1026 // Use a join to get the latest revision.
1027 // Note that we don't use newRevisionFromConds here because we don't want to retry
1028 // and fall back to master. The assumption is that we only want to force the fallback
1029 // if we are quite sure the revision exists because the caller supplied a revision ID.
1030 // If the page isn't found at all on a replica, it probably simply does not exist.
1031 $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
1032
1033 $conds[] = 'rev_id=page_latest';
1034 $rev = $this->loadRevisionFromConds( $db, $conds, $flags );
1035
1036 $this->releaseDBConnection( $db );
1037 return $rev;
1038 }
1039 }
1040
1052 public function getRevisionByTimestamp( $title, $timestamp ) {
1053 $db = $this->getDBConnection( DB_REPLICA );
1054 return $this->newRevisionFromConds(
1055 [
1056 'rev_timestamp' => $db->timestamp( $timestamp ),
1057 'page_namespace' => $title->getNamespace(),
1058 'page_title' => $title->getDBkey()
1059 ],
1060 0,
1061 $title
1062 );
1063 }
1064
1083 $row,
1084 $queryFlags = 0,
1085 Title $title = null,
1086 array $overrides = []
1087 ) {
1088 Assert::parameterType( 'object', $row, '$row' );
1089
1090 // check second argument, since Revision::newFromArchiveRow had $overrides in that spot.
1091 Assert::parameterType( 'integer', $queryFlags, '$queryFlags' );
1092
1093 if ( !$title && isset( $overrides['title'] ) ) {
1094 if ( !( $overrides['title'] instanceof Title ) ) {
1095 throw new MWException( 'title field override must contain a Title object.' );
1096 }
1097
1098 $title = $overrides['title'];
1099 }
1100
1101 if ( !isset( $title ) ) {
1102 if ( isset( $row->ar_namespace ) && isset( $row->ar_title ) ) {
1103 $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
1104 } else {
1105 throw new InvalidArgumentException(
1106 'A Title or ar_namespace and ar_title must be given'
1107 );
1108 }
1109 }
1110
1111 foreach ( $overrides as $key => $value ) {
1112 $field = "ar_$key";
1113 $row->$field = $value;
1114 }
1115
1116 try {
1118 isset( $row->ar_user ) ? $row->ar_user : null,
1119 isset( $row->ar_user_text ) ? $row->ar_user_text : null,
1120 isset( $row->ar_actor ) ? $row->ar_actor : null
1121 );
1122 } catch ( InvalidArgumentException $ex ) {
1123 wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
1124 $user = new UserIdentityValue( 0, '', 0 );
1125 }
1126
1127 $comment = $this->commentStore
1128 // Legacy because $row may have come from self::selectFields()
1129 ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'ar_comment', $row, true );
1130
1131 $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
1132 $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
1133
1134 return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
1135 }
1136
1150 private function newRevisionFromRow_1_29( $row, $queryFlags = 0, Title $title = null ) {
1151 Assert::parameterType( 'object', $row, '$row' );
1152
1153 if ( !$title ) {
1154 $pageId = isset( $row->rev_page ) ? $row->rev_page : 0; // XXX: also check page_id?
1155 $revId = isset( $row->rev_id ) ? $row->rev_id : 0;
1156
1157 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1158 }
1159
1160 if ( !isset( $row->page_latest ) ) {
1161 $row->page_latest = $title->getLatestRevID();
1162 if ( $row->page_latest === 0 && $title->exists() ) {
1163 wfWarn( 'Encountered title object in limbo: ID ' . $title->getArticleID() );
1164 }
1165 }
1166
1167 try {
1169 isset( $row->rev_user ) ? $row->rev_user : null,
1170 isset( $row->rev_user_text ) ? $row->rev_user_text : null,
1171 isset( $row->rev_actor ) ? $row->rev_actor : null
1172 );
1173 } catch ( InvalidArgumentException $ex ) {
1174 wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
1175 $user = new UserIdentityValue( 0, '', 0 );
1176 }
1177
1178 $comment = $this->commentStore
1179 // Legacy because $row may have come from self::selectFields()
1180 ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'rev_comment', $row, true );
1181
1182 $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
1183 $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
1184
1185 return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
1186 }
1187
1199 public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
1200 return $this->newRevisionFromRow_1_29( $row, $queryFlags, $title );
1201 }
1202
1218 array $fields,
1219 $queryFlags = 0,
1220 Title $title = null
1221 ) {
1222 if ( !$title && isset( $fields['title'] ) ) {
1223 if ( !( $fields['title'] instanceof Title ) ) {
1224 throw new MWException( 'title field must contain a Title object.' );
1225 }
1226
1227 $title = $fields['title'];
1228 }
1229
1230 if ( !$title ) {
1231 $pageId = isset( $fields['page'] ) ? $fields['page'] : 0;
1232 $revId = isset( $fields['id'] ) ? $fields['id'] : 0;
1233
1234 $title = $this->getTitle( $pageId, $revId, $queryFlags );
1235 }
1236
1237 if ( !isset( $fields['page'] ) ) {
1238 $fields['page'] = $title->getArticleID( $queryFlags );
1239 }
1240
1241 // if we have a content object, use it to set the model and type
1242 if ( !empty( $fields['content'] ) ) {
1243 if ( !( $fields['content'] instanceof Content ) ) {
1244 throw new MWException( 'content field must contain a Content object.' );
1245 }
1246
1247 if ( !empty( $fields['text_id'] ) ) {
1248 throw new MWException(
1249 "Text already stored in external store (id {$fields['text_id']}), " .
1250 "can't serialize content object"
1251 );
1252 }
1253 }
1254
1255 if (
1256 isset( $fields['comment'] )
1257 && !( $fields['comment'] instanceof CommentStoreComment )
1258 ) {
1259 $commentData = isset( $fields['comment_data'] ) ? $fields['comment_data'] : null;
1260
1261 if ( $fields['comment'] instanceof Message ) {
1262 $fields['comment'] = CommentStoreComment::newUnsavedComment(
1263 $fields['comment'],
1264 $commentData
1265 );
1266 } else {
1267 $commentText = trim( strval( $fields['comment'] ) );
1268 $fields['comment'] = CommentStoreComment::newUnsavedComment(
1269 $commentText,
1270 $commentData
1271 );
1272 }
1273 }
1274
1275 $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
1276
1277 $revision = new MutableRevisionRecord( $title, $this->wikiId );
1278 $this->initializeMutableRevisionFromArray( $revision, $fields );
1279 $revision->setSlot( $mainSlot );
1280
1281 return $revision;
1282 }
1283
1289 MutableRevisionRecord $record,
1290 array $fields
1291 ) {
1293 $user = null;
1294
1295 if ( isset( $fields['user'] ) && ( $fields['user'] instanceof UserIdentity ) ) {
1296 $user = $fields['user'];
1297 } else {
1298 try {
1300 isset( $fields['user'] ) ? $fields['user'] : null,
1301 isset( $fields['user_text'] ) ? $fields['user_text'] : null,
1302 isset( $fields['actor'] ) ? $fields['actor'] : null
1303 );
1304 } catch ( InvalidArgumentException $ex ) {
1305 $user = null;
1306 }
1307 }
1308
1309 if ( $user ) {
1310 $record->setUser( $user );
1311 }
1312
1313 $timestamp = isset( $fields['timestamp'] )
1314 ? strval( $fields['timestamp'] )
1315 : wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
1316
1317 $record->setTimestamp( $timestamp );
1318
1319 if ( isset( $fields['page'] ) ) {
1320 $record->setPageId( intval( $fields['page'] ) );
1321 }
1322
1323 if ( isset( $fields['id'] ) ) {
1324 $record->setId( intval( $fields['id'] ) );
1325 }
1326 if ( isset( $fields['parent_id'] ) ) {
1327 $record->setParentId( intval( $fields['parent_id'] ) );
1328 }
1329
1330 if ( isset( $fields['sha1'] ) ) {
1331 $record->setSha1( $fields['sha1'] );
1332 }
1333 if ( isset( $fields['size'] ) ) {
1334 $record->setSize( intval( $fields['size'] ) );
1335 }
1336
1337 if ( isset( $fields['minor_edit'] ) ) {
1338 $record->setMinorEdit( intval( $fields['minor_edit'] ) !== 0 );
1339 }
1340 if ( isset( $fields['deleted'] ) ) {
1341 $record->setVisibility( intval( $fields['deleted'] ) );
1342 }
1343
1344 if ( isset( $fields['comment'] ) ) {
1345 Assert::parameterType(
1346 CommentStoreComment::class,
1347 $fields['comment'],
1348 '$row[\'comment\']'
1349 );
1350 $record->setComment( $fields['comment'] );
1351 }
1352 }
1353
1368 public function loadRevisionFromId( IDatabase $db, $id ) {
1369 return $this->loadRevisionFromConds( $db, [ 'rev_id' => intval( $id ) ] );
1370 }
1371
1387 public function loadRevisionFromPageId( IDatabase $db, $pageid, $id = 0 ) {
1388 $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
1389 if ( $id ) {
1390 $conds['rev_id'] = intval( $id );
1391 } else {
1392 $conds[] = 'rev_id=page_latest';
1393 }
1394 return $this->loadRevisionFromConds( $db, $conds );
1395 }
1396
1413 public function loadRevisionFromTitle( IDatabase $db, $title, $id = 0 ) {
1414 if ( $id ) {
1415 $matchId = intval( $id );
1416 } else {
1417 $matchId = 'page_latest';
1418 }
1419
1420 return $this->loadRevisionFromConds(
1421 $db,
1422 [
1423 "rev_id=$matchId",
1424 'page_namespace' => $title->getNamespace(),
1425 'page_title' => $title->getDBkey()
1426 ],
1427 0,
1428 $title
1429 );
1430 }
1431
1447 public function loadRevisionFromTimestamp( IDatabase $db, $title, $timestamp ) {
1448 return $this->loadRevisionFromConds( $db,
1449 [
1450 'rev_timestamp' => $db->timestamp( $timestamp ),
1451 'page_namespace' => $title->getNamespace(),
1452 'page_title' => $title->getDBkey()
1453 ],
1454 0,
1455 $title
1456 );
1457 }
1458
1474 private function newRevisionFromConds( $conditions, $flags = 0, Title $title = null ) {
1475 $db = $this->getDBConnection( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
1476 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
1477 $this->releaseDBConnection( $db );
1478
1479 $lb = $this->getDBLoadBalancer();
1480
1481 // Make sure new pending/committed revision are visibile later on
1482 // within web requests to certain avoid bugs like T93866 and T94407.
1483 if ( !$rev
1484 && !( $flags & self::READ_LATEST )
1485 && $lb->getServerCount() > 1
1486 && $lb->hasOrMadeRecentMasterChanges()
1487 ) {
1488 $flags = self::READ_LATEST;
1489 $db = $this->getDBConnection( DB_MASTER );
1490 $rev = $this->loadRevisionFromConds( $db, $conditions, $flags, $title );
1491 $this->releaseDBConnection( $db );
1492 }
1493
1494 return $rev;
1495 }
1496
1510 private function loadRevisionFromConds(
1511 IDatabase $db,
1512 $conditions,
1513 $flags = 0,
1514 Title $title = null
1515 ) {
1516 $row = $this->fetchRevisionRowFromConds( $db, $conditions, $flags );
1517 if ( $row ) {
1518 $rev = $this->newRevisionFromRow( $row, $flags, $title );
1519
1520 return $rev;
1521 }
1522
1523 return null;
1524 }
1525
1533 private function checkDatabaseWikiId( IDatabase $db ) {
1534 $storeWiki = $this->wikiId;
1535 $dbWiki = $db->getDomainID();
1536
1537 if ( $dbWiki === $storeWiki ) {
1538 return;
1539 }
1540
1541 // XXX: we really want the default database ID...
1542 $storeWiki = $storeWiki ?: wfWikiID();
1543 $dbWiki = $dbWiki ?: wfWikiID();
1544
1545 if ( $dbWiki === $storeWiki ) {
1546 return;
1547 }
1548
1549 // HACK: counteract encoding imposed by DatabaseDomain
1550 $storeWiki = str_replace( '?h', '-', $storeWiki );
1551 $dbWiki = str_replace( '?h', '-', $dbWiki );
1552
1553 if ( $dbWiki === $storeWiki ) {
1554 return;
1555 }
1556
1557 throw new MWException( "RevisionStore for $storeWiki "
1558 . "cannot be used with a DB connection for $dbWiki" );
1559 }
1560
1573 private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
1574 $this->checkDatabaseWikiId( $db );
1575
1576 $revQuery = self::getQueryInfo( [ 'page', 'user' ] );
1577 $options = [];
1578 if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
1579 $options[] = 'FOR UPDATE';
1580 }
1581 return $db->selectRow(
1582 $revQuery['tables'],
1583 $revQuery['fields'],
1584 $conditions,
1585 __METHOD__,
1586 $options,
1587 $revQuery['joins']
1588 );
1589 }
1590
1609 public function getQueryInfo( $options = [] ) {
1610 $ret = [
1611 'tables' => [],
1612 'fields' => [],
1613 'joins' => [],
1614 ];
1615
1616 $ret['tables'][] = 'revision';
1617 $ret['fields'] = array_merge( $ret['fields'], [
1618 'rev_id',
1619 'rev_page',
1620 'rev_text_id',
1621 'rev_timestamp',
1622 'rev_minor_edit',
1623 'rev_deleted',
1624 'rev_len',
1625 'rev_parent_id',
1626 'rev_sha1',
1627 ] );
1628
1629 $commentQuery = $this->commentStore->getJoin( 'rev_comment' );
1630 $ret['tables'] = array_merge( $ret['tables'], $commentQuery['tables'] );
1631 $ret['fields'] = array_merge( $ret['fields'], $commentQuery['fields'] );
1632 $ret['joins'] = array_merge( $ret['joins'], $commentQuery['joins'] );
1633
1634 $actorQuery = $this->actorMigration->getJoin( 'rev_user' );
1635 $ret['tables'] = array_merge( $ret['tables'], $actorQuery['tables'] );
1636 $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
1637 $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
1638
1639 if ( $this->contentHandlerUseDB ) {
1640 $ret['fields'][] = 'rev_content_format';
1641 $ret['fields'][] = 'rev_content_model';
1642 }
1643
1644 if ( in_array( 'page', $options, true ) ) {
1645 $ret['tables'][] = 'page';
1646 $ret['fields'] = array_merge( $ret['fields'], [
1647 'page_namespace',
1648 'page_title',
1649 'page_id',
1650 'page_latest',
1651 'page_is_redirect',
1652 'page_len',
1653 ] );
1654 $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
1655 }
1656
1657 if ( in_array( 'user', $options, true ) ) {
1658 $ret['tables'][] = 'user';
1659 $ret['fields'] = array_merge( $ret['fields'], [
1660 'user_name',
1661 ] );
1662 $u = $actorQuery['fields']['rev_user'];
1663 $ret['joins']['user'] = [ 'LEFT JOIN', [ "$u != 0", "user_id = $u" ] ];
1664 }
1665
1666 if ( in_array( 'text', $options, true ) ) {
1667 $ret['tables'][] = 'text';
1668 $ret['fields'] = array_merge( $ret['fields'], [
1669 'old_text',
1670 'old_flags'
1671 ] );
1672 $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
1673 }
1674
1675 return $ret;
1676 }
1677
1691 public function getArchiveQueryInfo() {
1692 $commentQuery = $this->commentStore->getJoin( 'ar_comment' );
1693 $actorQuery = $this->actorMigration->getJoin( 'ar_user' );
1694 $ret = [
1695 'tables' => [ 'archive' ] + $commentQuery['tables'] + $actorQuery['tables'],
1696 'fields' => [
1697 'ar_id',
1698 'ar_page_id',
1699 'ar_namespace',
1700 'ar_title',
1701 'ar_rev_id',
1702 'ar_text_id',
1703 'ar_timestamp',
1704 'ar_minor_edit',
1705 'ar_deleted',
1706 'ar_len',
1707 'ar_parent_id',
1708 'ar_sha1',
1709 ] + $commentQuery['fields'] + $actorQuery['fields'],
1710 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
1711 ];
1712
1713 if ( $this->contentHandlerUseDB ) {
1714 $ret['fields'][] = 'ar_content_format';
1715 $ret['fields'][] = 'ar_content_model';
1716 }
1717
1718 return $ret;
1719 }
1720
1730 public function getRevisionSizes( array $revIds ) {
1731 return $this->listRevisionSizes( $this->getDBConnection( DB_REPLICA ), $revIds );
1732 }
1733
1746 public function listRevisionSizes( IDatabase $db, array $revIds ) {
1747 $this->checkDatabaseWikiId( $db );
1748
1749 $revLens = [];
1750 if ( !$revIds ) {
1751 return $revLens; // empty
1752 }
1753
1754 $res = $db->select(
1755 'revision',
1756 [ 'rev_id', 'rev_len' ],
1757 [ 'rev_id' => $revIds ],
1758 __METHOD__
1759 );
1760
1761 foreach ( $res as $row ) {
1762 $revLens[$row->rev_id] = intval( $row->rev_len );
1763 }
1764
1765 return $revLens;
1766 }
1767
1778 public function getPreviousRevision( RevisionRecord $rev, Title $title = null ) {
1779 if ( $title === null ) {
1780 $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
1781 }
1782 $prev = $title->getPreviousRevisionID( $rev->getId() );
1783 if ( $prev ) {
1784 return $this->getRevisionByTitle( $title, $prev );
1785 }
1786 return null;
1787 }
1788
1799 public function getNextRevision( RevisionRecord $rev, Title $title = null ) {
1800 if ( $title === null ) {
1801 $title = $this->getTitle( $rev->getPageId(), $rev->getId() );
1802 }
1803 $next = $title->getNextRevisionID( $rev->getId() );
1804 if ( $next ) {
1805 return $this->getRevisionByTitle( $title, $next );
1806 }
1807 return null;
1808 }
1809
1822 $this->checkDatabaseWikiId( $db );
1823
1824 if ( $rev->getPageId() === null ) {
1825 return 0;
1826 }
1827 # Use page_latest if ID is not given
1828 if ( !$rev->getId() ) {
1829 $prevId = $db->selectField(
1830 'page', 'page_latest',
1831 [ 'page_id' => $rev->getPageId() ],
1832 __METHOD__
1833 );
1834 } else {
1835 $prevId = $db->selectField(
1836 'revision', 'rev_id',
1837 [ 'rev_page' => $rev->getPageId(), 'rev_id < ' . $rev->getId() ],
1838 __METHOD__,
1839 [ 'ORDER BY' => 'rev_id DESC' ]
1840 );
1841 }
1842 return intval( $prevId );
1843 }
1844
1855 public function getTimestampFromId( $title, $id, $flags = 0 ) {
1856 $db = $this->getDBConnection(
1857 ( $flags & IDBAccessObject::READ_LATEST ) ? DB_MASTER : DB_REPLICA
1858 );
1859
1860 $conds = [ 'rev_id' => $id ];
1861 $conds['rev_page'] = $title->getArticleID();
1862 $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
1863
1864 $this->releaseDBConnection( $db );
1865 return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
1866 }
1867
1877 public function countRevisionsByPageId( IDatabase $db, $id ) {
1878 $this->checkDatabaseWikiId( $db );
1879
1880 $row = $db->selectRow( 'revision',
1881 [ 'revCount' => 'COUNT(*)' ],
1882 [ 'rev_page' => $id ],
1883 __METHOD__
1884 );
1885 if ( $row ) {
1886 return intval( $row->revCount );
1887 }
1888 return 0;
1889 }
1890
1900 public function countRevisionsByTitle( IDatabase $db, $title ) {
1901 $id = $title->getArticleID();
1902 if ( $id ) {
1903 return $this->countRevisionsByPageId( $db, $id );
1904 }
1905 return 0;
1906 }
1907
1926 public function userWasLastToEdit( IDatabase $db, $pageId, $userId, $since ) {
1927 $this->checkDatabaseWikiId( $db );
1928
1929 if ( !$userId ) {
1930 return false;
1931 }
1932
1934 $res = $db->select(
1935 $revQuery['tables'],
1936 [
1937 'rev_user' => $revQuery['fields']['rev_user'],
1938 ],
1939 [
1940 'rev_page' => $pageId,
1941 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
1942 ],
1943 __METHOD__,
1944 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
1945 $revQuery['joins']
1946 );
1947 foreach ( $res as $row ) {
1948 if ( $row->rev_user != $userId ) {
1949 return false;
1950 }
1951 }
1952 return true;
1953 }
1954
1968 public function getKnownCurrentRevision( Title $title, $revId ) {
1969 $db = $this->getDBConnectionRef( DB_REPLICA );
1970
1971 $pageId = $title->getArticleID();
1972
1973 if ( !$pageId ) {
1974 return false;
1975 }
1976
1977 if ( !$revId ) {
1978 $revId = $title->getLatestRevID();
1979 }
1980
1981 if ( !$revId ) {
1982 wfWarn(
1983 'No latest revision known for page ' . $title->getPrefixedDBkey()
1984 . ' even though it exists with page ID ' . $pageId
1985 );
1986 return false;
1987 }
1988
1989 $row = $this->cache->getWithSetCallback(
1990 // Page/rev IDs passed in from DB to reflect history merges
1991 $this->cache->makeGlobalKey( 'revision-row-1.29', $db->getDomainID(), $pageId, $revId ),
1992 WANObjectCache::TTL_WEEK,
1993 function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
1994 $setOpts += Database::getCacheSetOptions( $db );
1995
1996 $conds = [
1997 'rev_page' => intval( $pageId ),
1998 'page_id' => intval( $pageId ),
1999 'rev_id' => intval( $revId ),
2000 ];
2001
2002 $row = $this->fetchRevisionRowFromConds( $db, $conds );
2003 return $row ?: false; // don't cache negatives
2004 }
2005 );
2006
2007 // Reflect revision deletion and user renames
2008 if ( $row ) {
2009 return $this->newRevisionFromRow( $row, 0, $title );
2010 } else {
2011 return false;
2012 }
2013 }
2014
2015 // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
2016
2017}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
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.
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
This class handles the logic for the actor table migration.
CommentStoreComment represents a comment stored by CommentStore.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
A content handler knows how do deal with a specific type of content on a wiki page.
Helper class for DAO classes.
Hooks class.
Definition Hooks.php:34
A collection of public static functions to play with IP address and IP ranges.
Definition IP.php:67
MediaWiki exception.
Exception thrown when an unregistered content model is requested.
Exception representing a failure to access a data blob.
Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
Mutable RevisionRecord implementation, for building new revision entries programmatically.
setUser(UserIdentity $user)
Sets the user identity associated with the revision.
setSize( $size)
Set nominal revision size, for optimization.
setSha1( $sha1)
Set revision hash, for optimization.
Exception representing a failure to look up a revision.
A RevisionRecord representing a revision of a deleted page persisted in the archive table.
Page revision base class.
getComment( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision comment, if it's available to the specified audience.
getSize()
Returns the nominal size of this revision, in bogo-bytes.
getTimestamp()
MCR migration note: this replaces Revision::getTimestamp.
getSha1()
Returns the base36 sha1 of this revision.
getUser( $audience=self::FOR_PUBLIC, User $user=null)
Fetch revision's author's user identity, if it's available to the specified audience.
Value object representing the set of slots belonging to a revision.
A RevisionRecord representing an existing revision persisted in the revision table.
Service for looking up page revisions.
newRevisionFromConds( $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision.
userWasLastToEdit(IDatabase $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
loadSlotContent(SlotRecord $slot, $blobData=null, $blobFlags=null, $blobFormat=null, $queryFlags=0)
Loads a Content object based on a slot row.
getRcIdIfUnpatrolled(RevisionRecord $rev)
MCR migration note: this replaces Revision::isUnpatrolled.
countRevisionsByPageId(IDatabase $db, $id)
Get count of revisions per page...not very efficient.
getKnownCurrentRevision(Title $title, $revId)
Load a revision based on a known page ID and current revision ID from the DB.
releaseDBConnection(IDatabase $connection)
fetchRevisionRowFromConds(IDatabase $db, $conditions, $flags=0)
Given a set of conditions, return a row with the fields necessary to build RevisionRecord objects.
loadRevisionFromPageId(IDatabase $db, $pageid, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
loadRevisionFromId(IDatabase $db, $id)
Load a page revision from a given revision ID number.
newRevisionFromRow_1_29( $row, $queryFlags=0, Title $title=null)
initializeMutableRevisionFromArray(MutableRevisionRecord $record, array $fields)
countRevisionsByTitle(IDatabase $db, $title)
Get count of revisions per page...not very efficient.
getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object.
checkContentModel(Content $content, Title $title)
MCR migration note: this corresponds to Revision::checkContentModel.
loadRevisionFromTitle(IDatabase $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archived revision objec...
loadRevisionFromConds(IDatabase $db, $conditions, $flags=0, Title $title=null)
Given a set of conditions, fetch a revision from the given database connection.
setContentHandlerUseDB( $contentHandlerUseDB)
getPreviousRevisionId(IDatabase $db, RevisionRecord $rev)
Get previous revision Id for this page_id This is used to populate rev_parent_id on save.
loadRevisionFromTimestamp(IDatabase $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
getRevisionByTimestamp( $title, $timestamp)
Load the revision for the given title with the given timestamp.
listRevisionSizes(IDatabase $db, array $revIds)
Do a batched query for the sizes of a set of revisions.
setLogger(LoggerInterface $logger)
getRevisionByTitle(LinkTarget $linkTarget, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
getRevisionSizes(array $revIds)
Do a batched query for the sizes of a set of revisions.
getPreviousRevision(RevisionRecord $rev, Title $title=null)
Get previous revision for this title.
static mapArchiveFields( $archiveRow)
Maps fields of the archive row to corresponding revision rows.
newRevisionFromRow( $row, $queryFlags=0, Title $title=null)
getRecentChange(RevisionRecord $rev, $flags=0)
Get the RC object belonging to the current revision, if there's one.
__construct(LoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, ActorMigration $actorMigration, $wikiId=false)
getRevisionById( $id, $flags=0)
Load a page revision from a given revision ID number.
getTitle( $pageId, $revId, $queryFlags=self::READ_NORMAL)
Determines the page Title based on the available information.
newMutableRevisionFromArray(array $fields, $queryFlags=0, Title $title=null)
Constructs a new MutableRevisionRecord based on the given associative array following the MW1....
getRevisionByPageId( $pageId, $revId=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given page ID.
newRevisionFromArchiveRow( $row, $queryFlags=0, Title $title=null, array $overrides=[])
Make a fake revision object from an archive table row.
insertRevisionOn(RevisionRecord $rev, IDatabase $dbw)
Insert a new revision into the database, returning the new revision record on success and dies horrib...
getTimestampFromId( $title, $id, $flags=0)
Get rev_timestamp from rev_id, without loading the rest of the row.
checkDatabaseWikiId(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
newNullRevision(IDatabase $dbw, Title $title, CommentStoreComment $comment, $minor, User $user)
Create a new null-revision for insertion into a page's history.
getNextRevision(RevisionRecord $rev, Title $title=null)
Get next revision for this title.
emulateMainSlot_1_29( $row, $queryFlags, Title $title)
Constructs a RevisionRecord for the revisions main slot, based on the MW1.29 schema.
Value object representing a content slot associated with a page revision.
getModel()
Returns the content model.
getAddress()
Returns the address of this slot's content.
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot.
hasAddress()
Whether this slot has an address.
Service for storing and loading Content objects.
Value object representing a user's identity.
The Message class provides methods which fulfil two basic services:
Definition Message.php:159
Utility class for creating new RC entries.
const PRC_UNPATROLLED
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Represents a title within MediaWiki.
Definition Title.php:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:657
Multi-datacenter aware caching interface.
Helper class to handle automatically marking connections as reusable (via RAII pattern) as well handl...
Definition DBConnRef.php:15
Relational database abstraction object.
Definition Database.php:48
Database connection, tracking, load balancing, and transaction manager for a cluster.
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
when a variable name is used in a function
Definition design.txt:94
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
the array() calling protocol came about after MediaWiki 1.4rc1.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:964
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2005
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:903
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1777
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2176
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
Base interface for content objects.
Definition Content.php:34
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
getModel()
Returns the ID of the content model used by this Content object.
getDefaultFormat()
Convenience method that returns the default serialization format for the content model that this Cont...
isValid()
Returns whether the content is valid.
Interface for database access objects.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
const PARENT_HINT
Hint key for use with storeBlob, indicating the parent revision of the revision the blob is associate...
Definition BlobStore.php:64
const DESIGNATION_HINT
Hint key for use with storeBlob, indicating the general role the block takes in the application.
Definition BlobStore.php:40
const PAGE_HINT
Hint key for use with storeBlob, indicating the page the blob is associated with.
Definition BlobStore.php:46
const ROLE_HINT
Hint key for use with storeBlob, indicating the slot the blob is associated with.
Definition BlobStore.php:52
const FORMAT_HINT
Hint key for use with storeBlob, indicating the serialization format used to create the blob,...
Definition BlobStore.php:82
const SHA1_HINT
Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the method.
Definition BlobStore.php:70
const MODEL_HINT
Hint key for use with storeBlob, indicating the model of the content encoded in the given blob.
Definition BlobStore.php:76
Service for constructing revision objects.
Service for looking up page revisions.
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Single row SELECT wrapper.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
addQuotes( $s)
Adds quotes and backslashes.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
insertId()
Get the inserted value of an auto-increment row.
you have access to all of the normal MediaWiki so you can get a DB use the cache
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29