]> BookStack Code Mirror - bookstack/commitdiff
ZIP Imports: Built out reference parsing/updating logic
authorDan Brown <redacted>
Sun, 10 Nov 2024 16:03:50 +0000 (16:03 +0000)
committerDan Brown <redacted>
Sun, 10 Nov 2024 16:03:50 +0000 (16:03 +0000)
app/Entities/Repos/PageRepo.php
app/Exports/ZipExports/ZipExportReferences.php
app/Exports/ZipExports/ZipImportReferences.php [new file with mode: 0644]
app/Exports/ZipExports/ZipImportRunner.php
app/Exports/ZipExports/ZipReferenceParser.php

index 1bc15392cec7b4478372b761c4179e3fa364f297..68b1c398f801d22ac3d74211f2e10714505083f4 100644 (file)
@@ -87,6 +87,17 @@ class PageRepo
         return $draft;
     }
 
+    /**
+     * Directly update the content for the given page from the provided input.
+     * Used for direct content access in a way that performs required changes
+     * (Search index & reference regen) without performing an official update.
+     */
+    public function setContentFromInput(Page $page, array $input): void
+    {
+        $this->updateTemplateStatusAndContentFromInput($page, $input);
+        $this->baseRepo->update($page, []);
+    }
+
     /**
      * Update a page in the system.
      */
@@ -121,7 +132,7 @@ class PageRepo
         return $page;
     }
 
-    protected function updateTemplateStatusAndContentFromInput(Page $page, array $input)
+    protected function updateTemplateStatusAndContentFromInput(Page $page, array $input): void
     {
         if (isset($input['template']) && userCan('templates-manage')) {
             $page->template = ($input['template'] === 'true');
index c630c832b372370a3bad04f606d708e0c1968590..0de409fa19aaa0f35153f6a6243c5eae10265b77 100644 (file)
@@ -85,9 +85,9 @@ class ZipExportReferences
         // Parse page content first
         foreach ($this->pages as $page) {
             $handler = $createHandler($page);
-            $page->html = $this->parser->parse($page->html ?? '', $handler);
+            $page->html = $this->parser->parseLinks($page->html ?? '', $handler);
             if ($page->markdown) {
-                $page->markdown = $this->parser->parse($page->markdown, $handler);
+                $page->markdown = $this->parser->parseLinks($page->markdown, $handler);
             }
         }
 
@@ -95,7 +95,7 @@ class ZipExportReferences
         foreach ($this->chapters as $chapter) {
             if ($chapter->description_html) {
                 $handler = $createHandler($chapter);
-                $chapter->description_html = $this->parser->parse($chapter->description_html, $handler);
+                $chapter->description_html = $this->parser->parseLinks($chapter->description_html, $handler);
             }
         }
 
@@ -103,7 +103,7 @@ class ZipExportReferences
         foreach ($this->books as $book) {
             if ($book->description_html) {
                 $handler = $createHandler($book);
-                $book->description_html = $this->parser->parse($book->description_html, $handler);
+                $book->description_html = $this->parser->parseLinks($book->description_html, $handler);
             }
         }
     }
diff --git a/app/Exports/ZipExports/ZipImportReferences.php b/app/Exports/ZipExports/ZipImportReferences.php
new file mode 100644 (file)
index 0000000..8062886
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+
+namespace BookStack\Exports\ZipExports;
+
+use BookStack\App\Model;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\BaseRepo;
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exports\ZipExports\Models\ZipExportBook;
+use BookStack\Exports\ZipExports\Models\ZipExportChapter;
+use BookStack\Exports\ZipExports\Models\ZipExportPage;
+use BookStack\Uploads\Attachment;
+use BookStack\Uploads\Image;
+use BookStack\Uploads\ImageResizer;
+
+class ZipImportReferences
+{
+    /** @var Page[] */
+    protected array $pages = [];
+    /** @var Chapter[] */
+    protected array $chapters = [];
+    /** @var Book[] */
+    protected array $books = [];
+    /** @var Attachment[] */
+    protected array $attachments = [];
+    /** @var Image[] */
+    protected array $images = [];
+
+    /** @var array<string, Model> */
+    protected array $referenceMap = [];
+
+    /** @var array<int, ZipExportPage> */
+    protected array $zipExportPageMap = [];
+    /** @var array<int, ZipExportChapter> */
+    protected array $zipExportChapterMap = [];
+    /** @var array<int, ZipExportBook> */
+    protected array $zipExportBookMap = [];
+
+    public function __construct(
+        protected ZipReferenceParser $parser,
+        protected BaseRepo $baseRepo,
+        protected PageRepo $pageRepo,
+        protected ImageResizer $imageResizer,
+    ) {
+    }
+
+    protected function addReference(string $type, Model $model, ?int $importId): void
+    {
+        if ($importId) {
+            $key = $type . ':' . $importId;
+            $this->referenceMap[$key] = $model;
+        }
+    }
+
+    public function addPage(Page $page, ZipExportPage $exportPage): void
+    {
+        $this->pages[] = $page;
+        $this->zipExportPageMap[$page->id] = $exportPage;
+        $this->addReference('page', $page, $exportPage->id);
+    }
+
+    public function addChapter(Chapter $chapter, ZipExportChapter $exportChapter): void
+    {
+        $this->chapters[] = $chapter;
+        $this->zipExportChapterMap[$chapter->id] = $exportChapter;
+        $this->addReference('chapter', $chapter, $exportChapter->id);
+    }
+
+    public function addBook(Book $book, ZipExportBook $exportBook): void
+    {
+        $this->books[] = $book;
+        $this->zipExportBookMap[$book->id] = $exportBook;
+        $this->addReference('book', $book, $exportBook->id);
+    }
+
+    public function addAttachment(Attachment $attachment, ?int $importId): void
+    {
+        $this->attachments[] = $attachment;
+        $this->addReference('attachment', $attachment, $importId);
+    }
+
+    public function addImage(Image $image, ?int $importId): void
+    {
+        $this->images[] = $image;
+        $this->addReference('image', $image, $importId);
+    }
+
+    protected function handleReference(string $type, int $id): ?string
+    {
+        $key = $type . ':' . $id;
+        $model = $this->referenceMap[$key] ?? null;
+        if ($model instanceof Entity) {
+            return $model->getUrl();
+        } else if ($model instanceof Image) {
+            if ($model->type === 'gallery') {
+                $this->imageResizer->loadGalleryThumbnailsForImage($model, false);
+                return $model->thumbs['gallery'] ?? $model->url;
+            }
+
+            return $model->url;
+        }
+
+        return null;
+    }
+
+    public function replaceReferences(): void
+    {
+        foreach ($this->books as $book) {
+            $exportBook = $this->zipExportBookMap[$book->id];
+            $content = $exportBook->description_html || '';
+            $parsed = $this->parser->parseReferences($content, $this->handleReference(...));
+
+            $this->baseRepo->update($book, [
+                'description_html' => $parsed,
+            ]);
+        }
+
+        foreach ($this->chapters as $chapter) {
+            $exportChapter = $this->zipExportChapterMap[$chapter->id];
+            $content = $exportChapter->description_html || '';
+            $parsed = $this->parser->parseReferences($content, $this->handleReference(...));
+
+            $this->baseRepo->update($chapter, [
+                'description_html' => $parsed,
+            ]);
+        }
+
+        foreach ($this->pages as $page) {
+            $exportPage = $this->zipExportPageMap[$page->id];
+            $contentType = $exportPage->markdown ? 'markdown' : 'html';
+            $content = $exportPage->markdown ?: ($exportPage->html ?: '');
+            $parsed = $this->parser->parseReferences($content, $this->handleReference(...));
+
+            $this->pageRepo->setContentFromInput($page, [
+                $contentType => $parsed,
+            ]);
+        }
+    }
+}
index 2b897ff91673f9327e9c0854442bd62aad0bfb35..345c22be153b6e928879a1366d1ec750ba081299 100644 (file)
@@ -23,8 +23,6 @@ use Illuminate\Http\UploadedFile;
 class ZipImportRunner
 {
     protected array $tempFilesToCleanup = []; // TODO
-    protected array $createdImages = []; // TODO
-    protected array $createdAttachments = []; // TODO
 
     public function __construct(
         protected FileStorage $storage,
@@ -32,6 +30,7 @@ class ZipImportRunner
         protected ChapterRepo $chapterRepo,
         protected BookRepo $bookRepo,
         protected ImageService $imageService,
+        protected ZipImportReferences $references,
     ) {
     }
 
@@ -68,6 +67,11 @@ class ZipImportRunner
         // TODO - Run import
           // TODO - In transaction?
             // TODO - Revert uploaded files if goes wrong
+              // TODO - Attachments
+              // TODO - Images
+              // (Both listed/stored in references)
+
+        $this->references->replaceReferences();
     }
 
     protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
@@ -82,15 +86,17 @@ class ZipImportRunner
         // TODO - Parse/format description_html references
 
         if ($book->cover) {
-            $this->createdImages[] = $book->cover;
+            $this->references->addImage($book->cover, null);
         }
 
         // TODO - Pages
         foreach ($exportBook->chapters as $exportChapter) {
-            $this->importChapter($exportChapter, $book);
+            $this->importChapter($exportChapter, $book, $reader);
         }
         // TODO - Sort chapters/pages by order
 
+        $this->references->addBook($book, $exportBook);
+
         return $book;
     }
 
@@ -114,6 +120,8 @@ class ZipImportRunner
         }
         // TODO - Pages
 
+        $this->references->addChapter($chapter, $exportChapter);
+
         return $chapter;
     }
 
@@ -122,7 +130,9 @@ class ZipImportRunner
         $page = $this->pageRepo->getNewDraftPage($parent);
 
         // TODO - Import attachments
+          // TODO - Add attachment references
         // TODO - Import images
+          // TODO - Add image references
         // TODO - Parse/format HTML
 
         $this->pageRepo->publishDraft($page, [
@@ -132,6 +142,8 @@ class ZipImportRunner
             'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
         ]);
 
+        $this->references->addPage($page, $exportPage);
+
         return $page;
     }
 
index da43d1b366bbf9e5ffc30c26f52fe7bd1cdc900b..5929383b4dd77f29248f16ac3666ea6dcffd0bc2 100644 (file)
@@ -15,27 +15,23 @@ use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
 class ZipReferenceParser
 {
     /**
-     * @var CrossLinkModelResolver[]
+     * @var CrossLinkModelResolver[]|null
      */
-    protected array $modelResolvers;
+    protected ?array $modelResolvers = null;
 
-    public function __construct(EntityQueries $queries)
-    {
-        $this->modelResolvers = [
-            new PagePermalinkModelResolver($queries->pages),
-            new PageLinkModelResolver($queries->pages),
-            new ChapterLinkModelResolver($queries->chapters),
-            new BookLinkModelResolver($queries->books),
-            new ImageModelResolver(),
-            new AttachmentModelResolver(),
-        ];
+    public function __construct(
+        protected EntityQueries $queries
+    ) {
     }
 
     /**
      * Parse and replace references in the given content.
+     * Calls the handler for each model link detected and replaces the link
+     * with the handler return value if provided.
+     * Returns the resulting content with links replaced.
      * @param callable(Model):(string|null) $handler
      */
-    public function parse(string $content, callable $handler): string
+    public function parseLinks(string $content, callable $handler): string
     {
         $escapedBase = preg_quote(url('/'), '/');
         $linkRegex = "/({$escapedBase}.*?)[\\t\\n\\f>\"'=?#()]/";
@@ -59,13 +55,43 @@ class ZipReferenceParser
         return $content;
     }
 
+    /**
+     * Parse and replace references in the given content.
+     * Calls the handler for each reference detected and replaces the link
+     * with the handler return value if provided.
+     * Returns the resulting content string with references replaced.
+     * @param callable(string $type, int $id):(string|null) $handler
+     */
+    public function parseReferences(string $content, callable $handler): string
+    {
+        $referenceRegex = '/\[\[bsexport:([a-z]+):(\d+)]]/';
+        $matches = [];
+        preg_match_all($referenceRegex, $content, $matches);
+
+        if (count($matches) < 3) {
+            return $content;
+        }
+
+        for ($i = 0; $i < count($matches[0]); $i++) {
+            $referenceText = $matches[0][$i];
+            $type = strtolower($matches[1][$i]);
+            $id = intval($matches[2][$i]);
+            $result = $handler($type, $id);
+            if ($result !== null) {
+                $content = str_replace($referenceText, $result, $content);
+            }
+        }
+
+        return $content;
+    }
+
 
     /**
      * Attempt to resolve the given link to a model using the instance model resolvers.
      */
     protected function linkToModel(string $link): ?Model
     {
-        foreach ($this->modelResolvers as $resolver) {
+        foreach ($this->getModelResolvers() as $resolver) {
             $model = $resolver->resolve($link);
             if (!is_null($model)) {
                 return $model;
@@ -74,4 +100,22 @@ class ZipReferenceParser
 
         return null;
     }
+
+    protected function getModelResolvers(): array
+    {
+        if (isset($this->modelResolvers)) {
+            return $this->modelResolvers;
+        }
+
+        $this->modelResolvers = [
+            new PagePermalinkModelResolver($this->queries->pages),
+            new PageLinkModelResolver($this->queries->pages),
+            new ChapterLinkModelResolver($this->queries->chapters),
+            new BookLinkModelResolver($this->queries->books),
+            new ImageModelResolver(),
+            new AttachmentModelResolver(),
+        ];
+
+        return $this->modelResolvers;
+    }
 }