]> BookStack Code Mirror - bookstack/commitdiff
Copying: Added logic to find & update references
authorDan Brown <redacted>
Tue, 25 Nov 2025 17:52:26 +0000 (17:52 +0000)
committerDan Brown <redacted>
Tue, 25 Nov 2025 17:52:26 +0000 (17:52 +0000)
app/Entities/Models/Page.php
app/Entities/Tools/Cloner.php
app/References/ReferenceChangeContext.php
app/References/ReferenceUpdater.php
tests/Entity/CopyTest.php

index 88c59bd1bd033e854edb54f9a48288b0f5c54817..a1d3fc7b40d53338f269d05ede4e4b50f98bdd1a 100644 (file)
@@ -124,6 +124,14 @@ class Page extends BookChild
         return url('/' . implode('/', $parts));
     }
 
+    /**
+     * Get the ID-based permalink for this page.
+     */
+    public function getPermalink(): string
+    {
+        return url("/link/{$this->id}");
+    }
+
     /**
      * Get this page for JSON display.
      */
index 916ce42c572bccf281e244a05decf38365e9725d..64c48c351ae8ecca0e28739ec16d5cffefc048c5 100644 (file)
@@ -78,10 +78,12 @@ class Cloner
         if (userCan(Permission::PageCreate, $copyChapter)) {
             /** @var Page $page */
             foreach ($original->getVisiblePages() as $page) {
-                $this->clonePage($page, $copyChapter, $page->name);
+                $this->createPageClone($page, $copyChapter, $page->name);
             }
         }
 
+        $this->referenceChangeContext->add($original, $copyChapter);
+
         return $copyChapter;
     }
 
@@ -109,11 +111,11 @@ class Cloner
         $directChildren = $original->getDirectVisibleChildren();
         foreach ($directChildren as $child) {
             if ($child instanceof Chapter && userCan(Permission::ChapterCreate, $copyBook)) {
-                $this->cloneChapter($child, $copyBook, $child->name);
+                $this->createChapterClone($child, $copyBook, $child->name);
             }
 
             if ($child instanceof Page && !$child->draft && userCan(Permission::PageCreate, $copyBook)) {
-                $this->clonePage($child, $copyBook, $child->name);
+                $this->createPageClone($child, $copyBook, $child->name);
             }
         }
 
@@ -125,6 +127,8 @@ class Cloner
             }
         }
 
+        $this->referenceChangeContext->add($original, $copyBook);
+
         return $copyBook;
     }
 
index df3028b931a5ef2bc1ae2af0cc9140d5cf542400..f11619813691be4cf4baa618590b7699bf7173e7 100644 (file)
@@ -16,4 +16,41 @@ class ReferenceChangeContext
     {
         $this->changes[] = [$oldEntity, $newEntity];
     }
+
+    /**
+     * Get all the change pairs.
+     * Returned array is an array of pairs, where the first item is the old entity
+     * and the second is the new entity.
+     * @return array<array{0: Entity, 1: Entity}>
+     */
+    public function getChanges(): array
+    {
+        return $this->changes;
+    }
+
+    /**
+     * Get all the new entities from the changes.
+     */
+    public function getNewEntities(): array
+    {
+        return array_column($this->changes, 1);
+    }
+
+    /**
+     * Get all the old entities from the changes.
+     */
+    public function getOldEntities(): array
+    {
+        return array_column($this->changes, 0);
+    }
+
+    public function getNewForOld(Entity $oldEntity): ?Entity
+    {
+        foreach ($this->changes as [$old, $new]) {
+            if ($old->id === $oldEntity->id && $old->type === $oldEntity->type) {
+                return $new;
+            }
+        }
+        return null;
+    }
 }
index 3a6db05ef394d8b294153cd7bef8c3fa2853b727..b811fe868abca02982a8138b61b014a5f5df8126 100644 (file)
@@ -5,7 +5,6 @@ namespace BookStack\References;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\HasDescriptionInterface;
 use BookStack\Entities\Models\Entity;
-use BookStack\Entities\Models\EntityContainerData;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\RevisionRepo;
 use BookStack\Util\HtmlDocument;
@@ -30,12 +29,45 @@ class ReferenceUpdater
         }
     }
 
+    /**
+     * Change existing references for a range of entities using the given context.
+     */
     public function changeReferencesUsingContext(ReferenceChangeContext $context): void
     {
-        // TODO
+        $bindings = [];
+        foreach ($context->getOldEntities() as $old) {
+            $bindings[] = $old->getMorphClass();
+            $bindings[] = $old->id;
+        }
 
-        // We should probably have references by this point, so we could use those for efficient
-        // discovery instead of scanning each item within the context.
+        // No targets to update within the context, so no need to continue.
+        if (count($bindings) < 2) {
+            return;
+        }
+
+        $toReferenceQuery = '(to_type, to_id) IN (' . rtrim(str_repeat('(?,?),', count($bindings) / 2), ',') . ')';
+
+        // Cycle each new entity in the context
+        foreach ($context->getNewEntities() as $new) {
+            // For each, get all references from it which lead to other items within the context of the change
+            $newReferencesInContext = $new->referencesFrom()->whereRaw($toReferenceQuery, $bindings)->get();
+            // For each reference, update the URL and the reference entry
+            foreach ($newReferencesInContext as $reference) {
+                $oldToEntity = $reference->to;
+                $newToEntity = $context->getNewForOld($oldToEntity);
+                if ($newToEntity === null) {
+                    continue;
+                }
+
+                $this->updateReferencesWithinEntity($new, $oldToEntity->getUrl(), $newToEntity->getUrl());
+                if ($newToEntity instanceof Page && $oldToEntity instanceof Page) {
+                    $this->updateReferencesWithinPage($newToEntity, $oldToEntity->getPermalink(), $newToEntity->getPermalink());
+                }
+                $reference->to_id = $newToEntity->id;
+                $reference->to_type = $newToEntity->getMorphClass();
+                $reference->save();
+            }
+        }
     }
 
     /**
index 258b538f5aaf807822839057725cd1273fc65173..43fe120836fab374d7afdaabd8041dcc907d573e 100644 (file)
@@ -214,12 +214,12 @@ class CopyTest extends TestCase
             'html' => '<p>This is a test <a href="' . $book->getUrl() . '">book link</a></p>',
         ]);
 
-        $html = '<p>This is a test <a href="' . $page->getUrl() . '">page link</a></p>';
-
         // Quick pre-update to get stable slug
         $this->put($book->getUrl(), ['name' => 'Internal ref test']);
         $book->refresh();
+        $page->refresh();
 
+        $html = '<p>This is a test <a href="' . $page->getUrl() . '">page link</a></p>';
         $this->put($book->getUrl(), ['name' => 'Internal ref test', 'description_html' => $html]);
 
         $this->post($book->getUrl('/copy'), ['name' => 'My copied book']);
@@ -245,12 +245,12 @@ class CopyTest extends TestCase
             'html' => '<p>This is a test <a href="' . $chapter->getUrl() . '">chapter link</a></p>',
         ]);
 
-        $html = '<p>This is a test <a href="' . $page->getUrl() . '">page link</a></p>';
-
         // Quick pre-update to get stable slug
         $this->put($chapter->getUrl(), ['name' => 'Internal ref test']);
         $chapter->refresh();
+        $page->refresh();
 
+        $html = '<p>This is a test <a href="' . $page->getUrl() . '">page link</a></p>';
         $this->put($chapter->getUrl(), ['name' => 'Internal ref test', 'description_html' => $html]);
 
         $this->post($chapter->getUrl('/copy'), ['name' => 'My copied chapter']);
@@ -258,11 +258,11 @@ class CopyTest extends TestCase
         $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
         $newPage = $newChapter->pages()->where('name', '=', 'reference test page')->first();
 
-        $this->assertStringContainsString($newChapter->getUrl(), $newPage->html);
-        $this->assertStringContainsString($newPage->getUrl(), $newChapter->description_html);
+        $this->assertStringContainsString($newChapter->getUrl() . '"', $newPage->html);
+        $this->assertStringContainsString($newPage->getUrl() . '"', $newChapter->description_html);
 
-        $this->assertStringNotContainsString($chapter->getUrl(), $newPage->html);
-        $this->assertStringNotContainsString($page->getUrl(), $newChapter->description_html);
+        $this->assertStringNotContainsString($chapter->getUrl() . '"', $newPage->html);
+        $this->assertStringNotContainsString($page->getUrl() . '"', $newChapter->description_html);
     }
 
     public function test_page_copy_updates_internal_self_references()