diff --git a/app/Entities/Models/BookChild.php b/app/Entities/Models/BookChild.php index e1ba0b6f708..3b1ac1bab74 100644 --- a/app/Entities/Models/BookChild.php +++ b/app/Entities/Models/BookChild.php @@ -2,6 +2,7 @@ namespace BookStack\Entities\Models; +use BookStack\References\ReferenceUpdater; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -57,9 +58,15 @@ public function book(): BelongsTo */ public function changeBook(int $newBookId): Entity { + $oldUrl = $this->getUrl(); $this->book_id = $newBookId; $this->refreshSlug(); $this->save(); + + if ($oldUrl !== $this->getUrl()) { + app()->make(ReferenceUpdater::class)->updateEntityPageReferences($this, $oldUrl); + } + $this->refresh(); // Update all child pages if a chapter diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 39b90138352..cfde7fe1c57 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -6,6 +6,7 @@ use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\HasCoverImage; use BookStack\Exceptions\ImageUploadException; +use BookStack\References\ReferenceUpdater; use BookStack\Uploads\ImageRepo; use Illuminate\Http\UploadedFile; @@ -13,11 +14,13 @@ class BaseRepo { protected TagRepo $tagRepo; protected ImageRepo $imageRepo; + protected ReferenceUpdater $referenceUpdater; - public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo) + public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo, ReferenceUpdater $referenceUpdater) { $this->tagRepo = $tagRepo; $this->imageRepo = $imageRepo; + $this->referenceUpdater = $referenceUpdater; } /** @@ -48,6 +51,8 @@ public function create(Entity $entity, array $input) */ public function update(Entity $entity, array $input) { + $oldUrl = $entity->getUrl(); + $entity->fill($input); $entity->updated_by = user()->id; @@ -64,6 +69,10 @@ public function update(Entity $entity, array $input) $entity->rebuildPermissions(); $entity->indexForSearch(); + + if ($oldUrl !== $entity->getUrl()) { + $this->referenceUpdater->updateEntityPageReferences($entity, $oldUrl); + } } /** diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index e491d607094..c80cbdb149e 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -17,6 +17,7 @@ use BookStack\Exceptions\PermissionsException; use BookStack\Facades\Activity; use BookStack\References\ReferenceStore; +use BookStack\References\ReferenceUpdater; use Exception; use Illuminate\Pagination\LengthAwarePaginator; @@ -24,16 +25,23 @@ class PageRepo { protected BaseRepo $baseRepo; protected RevisionRepo $revisionRepo; - protected ReferenceStore $references; + protected ReferenceStore $referenceStore; + protected ReferenceUpdater $referenceUpdater; /** * PageRepo constructor. */ - public function __construct(BaseRepo $baseRepo, RevisionRepo $revisionRepo, ReferenceStore $references) + public function __construct( + BaseRepo $baseRepo, + RevisionRepo $revisionRepo, + ReferenceStore $referenceStore, + ReferenceUpdater $referenceUpdater + ) { $this->baseRepo = $baseRepo; $this->revisionRepo = $revisionRepo; - $this->references = $references; + $this->referenceStore = $referenceStore; + $this->referenceUpdater = $referenceUpdater; } /** @@ -127,11 +135,11 @@ public function getUserDraft(Page $page): ?PageRevision public function getNewDraftPage(Entity $parent) { $page = (new Page())->forceFill([ - 'name' => trans('entities.pages_initial_name'), + 'name' => trans('entities.pages_initial_name'), 'created_by' => user()->id, - 'owned_by' => user()->id, + 'owned_by' => user()->id, 'updated_by' => user()->id, - 'draft' => true, + 'draft' => true, ]); if ($parent instanceof Chapter) { @@ -158,12 +166,10 @@ public function publishDraft(Page $draft, array $input): Page $draft->draft = false; $draft->revision_count = 1; $draft->priority = $this->getNewPriority($draft); - $draft->refreshSlug(); $draft->save(); $this->revisionRepo->storeNewForPage($draft, trans('entities.pages_initial_revision')); - $draft->indexForSearch(); - $this->references->updateForPage($draft); + $this->referenceStore->updateForPage($draft); $draft->refresh(); Activity::add(ActivityType::PAGE_CREATE, $draft); @@ -183,7 +189,7 @@ public function update(Page $page, array $input): Page $this->updateTemplateStatusAndContentFromInput($page, $input); $this->baseRepo->update($page, $input); - $this->references->updateForPage($page); + $this->referenceStore->updateForPage($page); // Update with new details $page->revision_count++; @@ -283,6 +289,7 @@ public function destroy(Page $page) */ public function restoreRevision(Page $page, int $revisionId): Page { + $oldUrl = $page->getUrl(); $page->revision_count++; /** @var PageRevision $revision */ @@ -301,11 +308,15 @@ public function restoreRevision(Page $page, int $revisionId): Page $page->refreshSlug(); $page->save(); $page->indexForSearch(); - $this->references->updateForPage($page); + $this->referenceStore->updateForPage($page); $summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]); $this->revisionRepo->storeNewForPage($page, $summary); + if ($oldUrl !== $page->getUrl()) { + $this->referenceUpdater->updateEntityPageReferences($page, $oldUrl); + } + Activity::add(ActivityType::PAGE_RESTORE, $page); Activity::add(ActivityType::REVISION_RESTORE, $revision); diff --git a/app/References/CrossLinkReplacer.php b/app/References/ReferenceUpdater.php similarity index 95% rename from app/References/CrossLinkReplacer.php rename to app/References/ReferenceUpdater.php index 2df87fc8310..15619bc31c8 100644 --- a/app/References/CrossLinkReplacer.php +++ b/app/References/ReferenceUpdater.php @@ -8,7 +8,7 @@ use DOMDocument; use DOMXPath; -class CrossLinkReplacer +class ReferenceUpdater { protected ReferenceFetcher $referenceFetcher; protected RevisionRepo $revisionRepo; @@ -53,10 +53,10 @@ protected function updateLinksInMarkdown(string $markdown, string $oldLink, stri return $markdown; } - $commonLinkRegex = '/(\[.*?\]\()' . preg_quote($oldLink) . '(.*?\))/i'; + $commonLinkRegex = '/(\[.*?\]\()' . preg_quote($oldLink, '/') . '(.*?\))/i'; $markdown = preg_replace($commonLinkRegex, '$1' . $newLink . '$2', $markdown); - $referenceLinkRegex = '/(\[.*?\]:\s?)' . preg_quote($oldLink) . '(.*?)($|\s)/i'; + $referenceLinkRegex = '/(\[.*?\]:\s?)' . preg_quote($oldLink, '/') . '(.*?)($|\s)/i'; $markdown = preg_replace($referenceLinkRegex, '$1' . $newLink . '$2$3', $markdown); return $markdown; diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php index 9ae226bb71d..82cd1668075 100644 --- a/tests/References/ReferencesTest.php +++ b/tests/References/ReferencesTest.php @@ -2,6 +2,7 @@ namespace Tests\References; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\TrashCan; @@ -116,6 +117,64 @@ public function test_reference_page_shows_empty_state_with_no_references() ->assertSee('There are no tracked references'); } + public function test_pages_leading_to_entity_updated_on_url_change() + { + /** @var Page $pageA */ + /** @var Page $pageB */ + /** @var Book $book */ + $pageA = Page::query()->first(); + $pageB = Page::query()->where('id', '!=', $pageA->id)->first(); + $book = Book::query()->first(); + + foreach ([$pageA, $pageB] as $page) { + $page->html = 'Link'; + $page->save(); + $this->createReference($page, $book); + } + + $this->asEditor()->put($book->getUrl(), [ + 'name' => 'my updated book slugaroo', + ]); + + foreach ([$pageA, $pageB] as $page) { + $page->refresh(); + $this->assertStringContainsString('href="http://localhost/books/my-updated-book-slugaroo"', $page->html); + $this->assertDatabaseHas('page_revisions', [ + 'page_id' => $page->id, + 'summary' => 'System auto-update of internal links' + ]); + } + } + + public function test_markdown_links_leading_to_entity_updated_on_url_change() + { + /** @var Page $page */ + /** @var Book $book */ + $page = Page::query()->first(); + $book = Book::query()->first(); + + $bookUrl = $book->getUrl(); + $markdown = ' + [An awesome link](' . $bookUrl . ') + [An awesome link with query & hash](' . $bookUrl . '?test=yes#cats) + [An awesome link with path](' . $bookUrl . '/an/extra/trail) + [An awesome link with title](' . $bookUrl . ' "title") + [ref]: ' . $bookUrl . '?test=yes#dogs + [ref_without_space]:' . $bookUrl . ' + [ref_with_title]: ' . $bookUrl . ' "title"'; + $page->markdown = $markdown; + $page->save(); + $this->createReference($page, $book); + + $this->asEditor()->put($book->getUrl(), [ + 'name' => 'my updated book slugadoo', + ]); + + $page->refresh(); + $expected = str_replace($bookUrl, 'http://localhost/books/my-updated-book-slugadoo', $markdown); + $this->assertEquals($expected, $page->markdown); + } + protected function createReference(Model $from, Model $to) { (new Reference())->forceFill([