Merge "Update OOUI to v0.29.1"
[lhc/web/wiklou.git] / includes / Storage / RevisionStore.php
index d219267..d9db8bd 100644 (file)
@@ -48,6 +48,7 @@ use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 use RecentChange;
 use Revision;
+use RuntimeException;
 use stdClass;
 use Title;
 use User;
@@ -437,21 +438,25 @@ class RevisionStore
                $slotRoles = $rev->getSlotRoles();
 
                // Make sure the main slot is always provided throughout migration
-               if ( !in_array( 'main', $slotRoles ) ) {
+               if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
                        throw new InvalidArgumentException(
                                'main slot must be provided'
                        );
                }
 
                // If we are not writing into the new schema, we can't support extra slots.
-               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) && $slotRoles !== [ 'main' ] ) {
+               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW )
+                       && $slotRoles !== [ SlotRecord::MAIN ]
+               ) {
                        throw new InvalidArgumentException(
                                'Only the main slot is supported when not writing to the MCR enabled schema!'
                        );
                }
 
                // As long as we are not reading from the new schema, we don't want to write extra slots.
-               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) && $slotRoles !== [ 'main' ] ) {
+               if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW )
+                       && $slotRoles !== [ SlotRecord::MAIN ]
+               ) {
                        throw new InvalidArgumentException(
                                'Only the main slot is supported when not reading from the MCR enabled schema!'
                        );
@@ -466,6 +471,12 @@ class RevisionStore
                $this->failOnNull( $user->getId(), 'user field' );
                $this->failOnEmpty( $user->getName(), 'user_text field' );
 
+               if ( !$rev->isReadyForInsertion() ) {
+                       // This is here for future-proofing. At the time this check being added, it
+                       // was redundant to the individual checks above.
+                       throw new IncompleteRevisionException( 'Revision is incomplete' );
+               }
+
                // TODO: we shouldn't need an actual Title here.
                $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
                $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
@@ -513,17 +524,17 @@ class RevisionStore
                // Technically, this could go away after MCR migration: while
                // calling code may require a main slot to exist, RevisionStore
                // really should not know or care about that requirement.
-               $rev->getSlot( 'main', RevisionRecord::RAW );
+               $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
 
                foreach ( $slotRoles as $role ) {
                        $slot = $rev->getSlot( $role, RevisionRecord::RAW );
                        Assert::postcondition(
                                $slot->getContent() !== null,
-                               $role .  ' slot must have content'
+                               $role . ' slot must have content'
                        );
                        Assert::postcondition(
                                $slot->hasRevision(),
-                               $role .  ' slot must have a revision associated'
+                               $role . ' slot must have a revision associated'
                        );
                }
 
@@ -566,9 +577,14 @@ class RevisionStore
                foreach ( $slotRoles as $role ) {
                        $slot = $rev->getSlot( $role, RevisionRecord::RAW );
 
-                       if ( $slot->hasRevision() ) {
-                               // If the SlotRecord already has a revision ID set, this means it already exists
-                               // in the database, and should already belong to the current revision.
+                       // If the SlotRecord already has a revision ID set, this means it already exists
+                       // in the database, and should already belong to the current revision.
+                       // However, a slot may already have a revision, but no content ID, if the slot
+                       // is emulated based on the archive table, because we are in SCHEMA_COMPAT_READ_OLD
+                       // mode, and the respective archive row was not yet migrated to the new schema.
+                       // In that case, a new slot row (and content row) must be inserted even during
+                       // undeletion.
+                       if ( $slot->hasRevision() && $slot->hasContentId() ) {
                                // TODO: properly abort transaction if the assertion fails!
                                Assert::parameter(
                                        $slot->getRevision() === $revisionId,
@@ -583,7 +599,7 @@ class RevisionStore
                                $newSlots[$role] = $slot;
 
                                // Write the main slot's text ID to the revision table for backwards compatibility
-                               if ( $slot->getRole() === 'main'
+                               if ( $slot->getRole() === SlotRecord::MAIN
                                        && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
                                ) {
                                        $blobAddress = $slot->getAddress();
@@ -612,6 +628,8 @@ class RevisionStore
         * @param IDatabase $dbw
         * @param int $revisionId
         * @param string &$blobAddress (may change!)
+        *
+        * @return int the text row id
         */
        private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
                $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
@@ -631,6 +649,8 @@ class RevisionStore
                        [ 'rev_id' => $revisionId ],
                        __METHOD__
                );
+
+               return $textId;
        }
 
        /**
@@ -654,11 +674,16 @@ class RevisionStore
                        $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
                }
 
+               $contentId = null;
+
                // Write the main slot's text ID to the revision table for backwards compatibility
-               if ( $protoSlot->getRole() === 'main'
+               if ( $protoSlot->getRole() === SlotRecord::MAIN
                        && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD )
                ) {
-                       $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+                       // If SCHEMA_COMPAT_WRITE_NEW is also set, the fake content ID is overwritten
+                       // with the real content ID below.
+                       $textId = $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+                       $contentId = $this->emulateContentId( $textId );
                }
 
                if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
@@ -669,8 +694,6 @@ class RevisionStore
                        }
 
                        $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
-               } else {
-                       $contentId = null;
                }
 
                $savedSlot = SlotRecord::newSaved(
@@ -858,7 +881,7 @@ class RevisionStore
 
                if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_OLD ) ) {
                        // In non MCR mode this IF section will relate to the main slot
-                       $mainSlot = $rev->getSlot( 'main' );
+                       $mainSlot = $rev->getSlot( SlotRecord::MAIN );
                        $model = $mainSlot->getModel();
                        $format = $mainSlot->getFormat();
 
@@ -1036,13 +1059,15 @@ class RevisionStore
        ) {
                $this->checkDatabaseWikiId( $dbw );
 
+               $pageId = $title->getArticleID();
+
                // T51581: Lock the page table row to ensure no other process
                // is adding a revision to the page at the same time.
                // Avoid locking extra tables, compare T191892.
                $pageLatest = $dbw->selectField(
                        'page',
                        'page_latest',
-                       [ 'page_id' => $title->getArticleID() ],
+                       [ 'page_id' => $pageId ],
                        __METHOD__,
                        [ 'FOR UPDATE' ]
                );
@@ -1059,6 +1084,15 @@ class RevisionStore
                        $title
                );
 
+               if ( !$oldRevision ) {
+                       $msg = "Failed to load latest revision ID $pageLatest of page ID $pageId.";
+                       $this->logger->error(
+                               $msg,
+                               [ 'exception' => new RuntimeException( $msg ) ]
+                       );
+                       return null;
+               }
+
                // Construct the new revision
                $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
                $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
@@ -1191,9 +1225,10 @@ class RevisionStore
         */
        private function emulateMainSlot_1_29( $row, $queryFlags, Title $title ) {
                $mainSlotRow = new stdClass();
-               $mainSlotRow->role_name = 'main';
+               $mainSlotRow->role_name = SlotRecord::MAIN;
                $mainSlotRow->model_name = null;
                $mainSlotRow->slot_revision_id = null;
+               $mainSlotRow->slot_content_id = null;
                $mainSlotRow->content_address = null;
 
                $content = null;
@@ -1244,6 +1279,12 @@ class RevisionStore
                        $mainSlotRow->format_name = isset( $row->rev_content_format )
                                ? strval( $row->rev_content_format )
                                : null;
+
+                       if ( isset( $row->rev_text_id ) && intval( $row->rev_text_id ) > 0 ) {
+                               // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+                               $mainSlotRow->slot_content_id
+                                       = $this->emulateContentId( intval( $row->rev_text_id ) );
+                       }
                } elseif ( is_array( $row ) ) {
                        $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
 
@@ -1283,6 +1324,12 @@ class RevisionStore
                                        $mainSlotRow->format_name = $handler->getDefaultFormat();
                                }
                        }
+
+                       if ( isset( $row['text_id'] ) && intval( $row['text_id'] ) > 0 ) {
+                               // Overwritten below for SCHEMA_COMPAT_WRITE_NEW
+                               $mainSlotRow->slot_content_id
+                                       = $this->emulateContentId( intval( $row['text_id'] ) );
+                       }
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
@@ -1320,18 +1367,38 @@ class RevisionStore
                        };
                }
 
-               // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
-               // the inherited slot to have the same content_id as the original slot. In that case,
-               // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
-               $mainSlotRow->slot_content_id =
-                       function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
-                               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
-                               return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, 'main' );
-                       };
+               if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) {
+                       // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
+                       // the inherited slot to have the same content_id as the original slot. In that case,
+                       // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
+                       $mainSlotRow->slot_content_id =
+                               function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
+                                       $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+                                       return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, SlotRecord::MAIN );
+                               };
+               }
 
                return new SlotRecord( $mainSlotRow, $content );
        }
 
+       /**
+        * Provides a content ID to use with emulated SlotRecords in SCHEMA_COMPAT_OLD mode,
+        * based on the revision's text ID (rev_text_id or ar_text_id, respectively).
+        * Note that in SCHEMA_COMPAT_WRITE_BOTH, a callback to findSlotContentId() should be used
+        * instead, since in that mode, some revision rows may already have a real content ID,
+        * while other's don't - and for the ones that don't, we should indicate that it
+        * is missing and cause SlotRecords::hasContentId() to return false.
+        *
+        * @param int $textId
+        * @return int The emulated content ID
+        */
+       private function emulateContentId( $textId ) {
+               // Return a negative number to ensure the ID is distinct from any real content IDs
+               // that will be assigned in SCHEMA_COMPAT_WRITE_NEW mode and read in SCHEMA_COMPAT_READ_NEW
+               // mode.
+               return -$textId;
+       }
+
        /**
         * Loads a Content object based on a slot row.
         *
@@ -1558,7 +1625,7 @@ class RevisionStore
                        $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
                }
 
-               if ( !isset( $slots['main'] ) ) {
+               if ( !isset( $slots[SlotRecord::MAIN] ) ) {
                        throw new RevisionAccessException(
                                'Main slot of revision ' . $revId . ' not found in database!'
                        );
@@ -1589,7 +1656,7 @@ class RevisionStore
        ) {
                if ( !$this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_NEW ) ) {
                        $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
-                       $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+                       $slots = new RevisionSlots( [ SlotRecord::MAIN => $mainSlot ] );
                } else {
                        // XXX: do we need the same kind of caching here
                        // that getKnownCurrentRevision uses (if $revId == page_latest?)
@@ -1660,8 +1727,8 @@ class RevisionStore
                                $row->ar_actor ?? null
                        );
                } catch ( InvalidArgumentException $ex ) {
-                       wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
-                       $user = new UserIdentityValue( 0, '', 0 );
+                       wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+                       $user = new UserIdentityValue( 0, 'Unknown user', 0 );
                }
 
                $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
@@ -1708,8 +1775,8 @@ class RevisionStore
                                $row->rev_actor ?? null
                        );
                } catch ( InvalidArgumentException $ex ) {
-                       wfWarn( __METHOD__ . ': ' . $ex->getMessage() );
-                       $user = new UserIdentityValue( 0, '', 0 );
+                       wfWarn( __METHOD__ . ': ' . $title->getPrefixedDBkey() . ': ' . $ex->getMessage() );
+                       $user = new UserIdentityValue( 0, 'Unknown user', 0 );
                }
 
                $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
@@ -2295,7 +2362,7 @@ class RevisionStore
                        $ret['fields']['slot_revision_id'] = 'slots.rev_id';
                        $ret['fields']['slot_content_id'] = 'NULL';
                        $ret['fields']['slot_origin'] = 'slots.rev_id';
-                       $ret['fields']['role_name'] = $db->addQuotes( 'main' );
+                       $ret['fields']['role_name'] = $db->addQuotes( SlotRecord::MAIN );
 
                        if ( in_array( 'content', $options, true ) ) {
                                $ret['fields']['content_size'] = 'slots.rev_len';