diff --git a/app/Exports/ZipExportBuilder.php b/app/Exports/ZipExportBuilder.php index 2b8b45d0d..720b4997d 100644 --- a/app/Exports/ZipExportBuilder.php +++ b/app/Exports/ZipExportBuilder.php @@ -2,10 +2,9 @@ namespace BookStack\Exports; -use BookStack\Activity\Models\Tag; use BookStack\Entities\Models\Page; use BookStack\Exceptions\ZipExportException; -use BookStack\Uploads\Attachment; +use BookStack\Exports\ZipExportModels\ZipExportPage; use ZipArchive; class ZipExportBuilder @@ -13,7 +12,8 @@ class ZipExportBuilder protected array $data = []; public function __construct( - protected ZipExportFiles $files + protected ZipExportFiles $files, + protected ZipExportReferences $references, ) { } @@ -22,57 +22,21 @@ class ZipExportBuilder */ public function buildForPage(Page $page): string { - $this->data['page'] = $this->convertPage($page); + $exportPage = ZipExportPage::fromModel($page, $this->files); + $this->data['page'] = $exportPage; + + $this->references->addPage($exportPage); + return $this->build(); } - protected function convertPage(Page $page): array - { - $tags = array_map($this->convertTag(...), $page->tags()->get()->all()); - $attachments = array_map($this->convertAttachment(...), $page->attachments()->get()->all()); - - return [ - 'id' => $page->id, - 'name' => $page->name, - 'html' => '', // TODO - 'markdown' => '', // TODO - 'priority' => $page->priority, - 'attachments' => $attachments, - 'images' => [], // TODO - 'tags' => $tags, - ]; - } - - protected function convertAttachment(Attachment $attachment): array - { - $data = [ - 'name' => $attachment->name, - 'order' => $attachment->order, - ]; - - if ($attachment->external) { - $data['link'] = $attachment->path; - } else { - $data['file'] = $this->files->referenceForAttachment($attachment); - } - - return $data; - } - - protected function convertTag(Tag $tag): array - { - return [ - 'name' => $tag->name, - 'value' => $tag->value, - 'order' => $tag->order, - ]; - } - /** * @throws ZipExportException */ protected function build(): string { + $this->references->buildReferences(); + $this->data['exported_at'] = date(DATE_ATOM); $this->data['instance'] = [ 'version' => trim(file_get_contents(base_path('version'))), diff --git a/app/Exports/ZipExportModels/ZipExportAttachment.php b/app/Exports/ZipExportModels/ZipExportAttachment.php new file mode 100644 index 000000000..d6d674a91 --- /dev/null +++ b/app/Exports/ZipExportModels/ZipExportAttachment.php @@ -0,0 +1,37 @@ +<?php + +namespace BookStack\Exports\ZipExportModels; + +use BookStack\Exports\ZipExportFiles; +use BookStack\Uploads\Attachment; + +class ZipExportAttachment implements ZipExportModel +{ + public ?int $id = null; + public string $name; + public ?int $order = null; + public ?string $link = null; + public ?string $file = null; + + public static function fromModel(Attachment $model, ZipExportFiles $files): self + { + $instance = new self(); + $instance->id = $model->id; + $instance->name = $model->name; + + if ($model->external) { + $instance->link = $model->path; + } else { + $instance->file = $files->referenceForAttachment($model); + } + + return $instance; + } + + public static function fromModelArray(array $attachmentArray, ZipExportFiles $files): array + { + return array_values(array_map(function (Attachment $attachment) use ($files) { + return self::fromModel($attachment, $files); + }, $attachmentArray)); + } +} diff --git a/app/Exports/ZipExportModels/ZipExportImage.php b/app/Exports/ZipExportModels/ZipExportImage.php new file mode 100644 index 000000000..73fe3bbf5 --- /dev/null +++ b/app/Exports/ZipExportModels/ZipExportImage.php @@ -0,0 +1,11 @@ +<?php + +namespace BookStack\Exports\ZipExportModels; + +use BookStack\Activity\Models\Tag; + +class ZipExportImage implements ZipExportModel +{ + public string $name; + public string $file; +} diff --git a/app/Exports/ZipExportModels/ZipExportModel.php b/app/Exports/ZipExportModels/ZipExportModel.php new file mode 100644 index 000000000..e1cb616de --- /dev/null +++ b/app/Exports/ZipExportModels/ZipExportModel.php @@ -0,0 +1,11 @@ +<?php + +namespace BookStack\Exports\ZipExportModels; + +use BookStack\App\Model; +use BookStack\Exports\ZipExportFiles; + +interface ZipExportModel +{ +// public static function fromModel(Model $model, ZipExportFiles $files): self; +} diff --git a/app/Exports/ZipExportModels/ZipExportPage.php b/app/Exports/ZipExportModels/ZipExportPage.php new file mode 100644 index 000000000..6589ce60a --- /dev/null +++ b/app/Exports/ZipExportModels/ZipExportPage.php @@ -0,0 +1,39 @@ +<?php + +namespace BookStack\Exports\ZipExportModels; + +use BookStack\Entities\Models\Page; +use BookStack\Entities\Tools\PageContent; +use BookStack\Exports\ZipExportFiles; + +class ZipExportPage implements ZipExportModel +{ + public ?int $id = null; + public string $name; + public ?string $html = null; + public ?string $markdown = null; + public ?int $priority = null; + /** @var ZipExportAttachment[] */ + public array $attachments = []; + /** @var ZipExportImage[] */ + public array $images = []; + /** @var ZipExportTag[] */ + public array $tags = []; + + public static function fromModel(Page $model, ZipExportFiles $files): self + { + $instance = new self(); + $instance->id = $model->id; + $instance->name = $model->name; + $instance->html = (new PageContent($model))->render(); + + if (!empty($model->markdown)) { + $instance->markdown = $model->markdown; + } + + $instance->tags = ZipExportTag::fromModelArray($model->tags()->get()->all()); + $instance->attachments = ZipExportAttachment::fromModelArray($model->attachments()->get()->all(), $files); + + return $instance; + } +} diff --git a/app/Exports/ZipExportModels/ZipExportTag.php b/app/Exports/ZipExportModels/ZipExportTag.php new file mode 100644 index 000000000..636c9ff6d --- /dev/null +++ b/app/Exports/ZipExportModels/ZipExportTag.php @@ -0,0 +1,27 @@ +<?php + +namespace BookStack\Exports\ZipExportModels; + +use BookStack\Activity\Models\Tag; + +class ZipExportTag implements ZipExportModel +{ + public string $name; + public ?string $value = null; + public ?int $order = null; + + public static function fromModel(Tag $model): self + { + $instance = new self(); + $instance->name = $model->name; + $instance->value = $model->value; + $instance->order = $model->order; + + return $instance; + } + + public static function fromModelArray(array $tagArray): array + { + return array_values(array_map(self::fromModel(...), $tagArray)); + } +} diff --git a/app/Exports/ZipExportReferences.php b/app/Exports/ZipExportReferences.php new file mode 100644 index 000000000..89deb7eda --- /dev/null +++ b/app/Exports/ZipExportReferences.php @@ -0,0 +1,55 @@ +<?php + +namespace BookStack\Exports; + +use BookStack\App\Model; +use BookStack\Exports\ZipExportModels\ZipExportAttachment; +use BookStack\Exports\ZipExportModels\ZipExportPage; + +class ZipExportReferences +{ + /** @var ZipExportPage[] */ + protected array $pages = []; + protected array $books = []; + protected array $chapters = []; + + /** @var ZipExportAttachment[] */ + protected array $attachments = []; + + public function __construct( + protected ZipReferenceParser $parser, + ) { + } + + public function addPage(ZipExportPage $page): void + { + if ($page->id) { + $this->pages[$page->id] = $page; + } + + foreach ($page->attachments as $attachment) { + if ($attachment->id) { + $this->attachments[$attachment->id] = $attachment; + } + } + } + + public function buildReferences(): void + { + // TODO - References to images, attachments, other entities + + // TODO - Parse page MD & HTML + foreach ($this->pages as $page) { + $page->html = $this->parser->parse($page->html ?? '', function (Model $model): ?string { + // TODO - Handle found link to $model + // - Validate we can see/access $model, or/and that it's + // part of the export in progress. + return '[CAT]'; + }); + // TODO - markdown + } + + // TODO - Parse chapter desc html + // TODO - Parse book desc html + } +} diff --git a/app/Exports/ZipReferenceParser.php b/app/Exports/ZipReferenceParser.php new file mode 100644 index 000000000..6ca826bc3 --- /dev/null +++ b/app/Exports/ZipReferenceParser.php @@ -0,0 +1,75 @@ +<?php + +namespace BookStack\Exports; + +use BookStack\App\Model; +use BookStack\Entities\Queries\EntityQueries; +use BookStack\References\ModelResolvers\BookLinkModelResolver; +use BookStack\References\ModelResolvers\ChapterLinkModelResolver; +use BookStack\References\ModelResolvers\CrossLinkModelResolver; +use BookStack\References\ModelResolvers\PageLinkModelResolver; +use BookStack\References\ModelResolvers\PagePermalinkModelResolver; + +class ZipReferenceParser +{ + /** + * @var CrossLinkModelResolver[] + */ + protected array $modelResolvers; + + public function __construct(EntityQueries $queries) + { + $this->modelResolvers = [ + new PagePermalinkModelResolver($queries->pages), + new PageLinkModelResolver($queries->pages), + new ChapterLinkModelResolver($queries->chapters), + new BookLinkModelResolver($queries->books), + // TODO - Image + // TODO - Attachment + ]; + } + + /** + * Parse and replace references in the given content. + * @param callable(Model):(string|null) $handler + */ + public function parse(string $content, callable $handler): string + { + $escapedBase = preg_quote(url('/'), '/'); + $linkRegex = "/({$escapedBase}.*?)[\\t\\n\\f>\"'=?#]/"; + $matches = []; + preg_match_all($linkRegex, $content, $matches); + + if (count($matches) < 2) { + return $content; + } + + foreach ($matches[1] as $link) { + $model = $this->linkToModel($link); + if ($model) { + $result = $handler($model); + if ($result !== null) { + $content = str_replace($link, $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) { + $model = $resolver->resolve($link); + if (!is_null($model)) { + return $model; + } + } + + return null; + } +} diff --git a/dev/docs/portable-zip-file-format.md b/dev/docs/portable-zip-file-format.md index d5635bd39..7a99563d1 100644 --- a/dev/docs/portable-zip-file-format.md +++ b/dev/docs/portable-zip-file-format.md @@ -128,6 +128,7 @@ File must be an image type accepted by BookStack (png, jpg, gif, webp) #### Attachment +- `id` - Number, optional, original ID for the attachment from exported system. - `name` - String, required, name of attachment. - `link` - String, semi-optional, URL of attachment. - `file` - String reference, semi-optional, reference to attachment file.