ZIP Imports: Built out reference parsing/updating logic

This commit is contained in:
Dan Brown 2024-11-10 16:03:50 +00:00
parent d13e4d2eef
commit 378f0d595f
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
5 changed files with 232 additions and 23 deletions

View 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');

View 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);
}
}
}

View File

@ -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,
]);
}
}
}

View 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;
}

View 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;
}
}