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.
*/
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');
// 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);
}
}
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);
}
}
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);
}
}
}
--- /dev/null
+<?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,
+ ]);
+ }
+ }
+}
class ZipImportRunner
{
protected array $tempFilesToCleanup = []; // TODO
- protected array $createdImages = []; // TODO
- protected array $createdAttachments = []; // TODO
public function __construct(
protected FileStorage $storage,
protected ChapterRepo $chapterRepo,
protected BookRepo $bookRepo,
protected ImageService $imageService,
+ protected ZipImportReferences $references,
) {
}
// 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
// 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;
}
}
// TODO - Pages
+ $this->references->addChapter($chapter, $exportChapter);
+
return $chapter;
}
$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, [
'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
]);
+ $this->references->addPage($page, $exportPage);
+
return $page;
}
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>\"'=?#()]/";
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;
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;
+ }
}