3 namespace BookStack\Exports\ZipExports;
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\Chapter;
7 use BookStack\Entities\Models\Entity;
8 use BookStack\Entities\Models\Page;
9 use BookStack\Entities\Repos\BookRepo;
10 use BookStack\Entities\Repos\ChapterRepo;
11 use BookStack\Entities\Repos\PageRepo;
12 use BookStack\Exceptions\ZipExportException;
13 use BookStack\Exceptions\ZipImportException;
14 use BookStack\Exports\Import;
15 use BookStack\Exports\ZipExports\Models\ZipExportBook;
16 use BookStack\Exports\ZipExports\Models\ZipExportChapter;
17 use BookStack\Exports\ZipExports\Models\ZipExportPage;
18 use BookStack\Exports\ZipExports\Models\ZipExportTag;
19 use BookStack\Uploads\FileStorage;
20 use BookStack\Uploads\ImageService;
21 use Illuminate\Http\UploadedFile;
25 protected array $tempFilesToCleanup = []; // TODO
27 public function __construct(
28 protected FileStorage $storage,
29 protected PageRepo $pageRepo,
30 protected ChapterRepo $chapterRepo,
31 protected BookRepo $bookRepo,
32 protected ImageService $imageService,
33 protected ZipImportReferences $references,
38 * @throws ZipImportException
40 public function run(Import $import, ?Entity $parent = null): void
42 $zipPath = $this->getZipPath($import);
43 $reader = new ZipExportReader($zipPath);
45 $errors = (new ZipExportValidator($reader))->validate();
47 throw new ZipImportException(["ZIP failed to validate"]);
51 $exportModel = $reader->decodeDataToExportModel();
52 } catch (ZipExportException $e) {
53 throw new ZipImportException([$e->getMessage()]);
56 // Validate parent type
57 if ($exportModel instanceof ZipExportBook && ($parent !== null)) {
58 throw new ZipImportException(["Must not have a parent set for a Book import"]);
59 } else if ($exportModel instanceof ZipExportChapter && (!$parent instanceof Book)) {
60 throw new ZipImportException(["Parent book required for chapter import"]);
61 } else if ($exportModel instanceof ZipExportPage && !($parent instanceof Book || $parent instanceof Chapter)) {
62 throw new ZipImportException(["Parent book or chapter required for page import"]);
65 $this->ensurePermissionsPermitImport($exportModel);
68 // TODO - In transaction?
69 // TODO - Revert uploaded files if goes wrong
72 // (Both listed/stored in references)
74 $this->references->replaceReferences();
77 protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
79 $book = $this->bookRepo->create([
80 'name' => $exportBook->name,
81 'description_html' => $exportBook->description_html ?? '',
82 'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null,
83 'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []),
86 // TODO - Parse/format description_html references
89 $this->references->addImage($book->cover, null);
93 foreach ($exportBook->chapters as $exportChapter) {
94 $this->importChapter($exportChapter, $book, $reader);
96 // TODO - Sort chapters/pages by order
98 $this->references->addBook($book, $exportBook);
103 protected function importChapter(ZipExportChapter $exportChapter, Book $parent, ZipExportReader $reader): Chapter
105 $chapter = $this->chapterRepo->create([
106 'name' => $exportChapter->name,
107 'description_html' => $exportChapter->description_html ?? '',
108 'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
111 // TODO - Parse/format description_html references
113 $exportPages = $exportChapter->pages;
114 usort($exportPages, function (ZipExportPage $a, ZipExportPage $b) {
115 return ($a->priority ?? 0) - ($b->priority ?? 0);
118 foreach ($exportPages as $exportPage) {
123 $this->references->addChapter($chapter, $exportChapter);
128 protected function importPage(ZipExportPage $exportPage, Book|Chapter $parent, ZipExportReader $reader): Page
130 $page = $this->pageRepo->getNewDraftPage($parent);
132 // TODO - Import attachments
133 // TODO - Add attachment references
134 // TODO - Import images
135 // TODO - Add image references
136 // TODO - Parse/format HTML
138 $this->pageRepo->publishDraft($page, [
139 'name' => $exportPage->name,
140 'markdown' => $exportPage->markdown,
141 'html' => $exportPage->html,
142 'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
145 $this->references->addPage($page, $exportPage);
150 protected function exportTagsToInputArray(array $exportTags): array
154 /** @var ZipExportTag $tag */
155 foreach ($exportTags as $tag) {
156 $tags[] = ['name' => $tag->name, 'value' => $tag->value ?? ''];
162 protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile
164 $tempPath = tempnam(sys_get_temp_dir(), 'bszipextract');
165 $fileStream = $reader->streamFile($fileName);
166 $tempStream = fopen($tempPath, 'wb');
167 stream_copy_to_stream($fileStream, $tempStream);
170 $this->tempFilesToCleanup[] = $tempPath;
172 return new UploadedFile($tempPath, $fileName);
176 * @throws ZipImportException
178 protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|ZipExportBook $exportModel, Book|Chapter|null $parent = null): void
182 // TODO - Extract messages to language files
183 // TODO - Ensure these are shown to users on failure
190 if ($exportModel instanceof ZipExportBook) {
191 if (!userCan('book-create-all')) {
192 $errors[] = 'You are lacking the required permission to create books.';
194 array_push($pages, ...$exportModel->pages);
195 array_push($chapters, ...$exportModel->chapters);
196 } else if ($exportModel instanceof ZipExportChapter) {
197 $chapters[] = $exportModel;
198 } else if ($exportModel instanceof ZipExportPage) {
199 $pages[] = $exportModel;
202 foreach ($chapters as $chapter) {
203 array_push($pages, ...$chapter->pages);
206 if (count($chapters) > 0) {
207 $permission = 'chapter-create' . ($parent ? '' : '-all');
208 if (!userCan($permission, $parent)) {
209 $errors[] = 'You are lacking the required permission to create chapters.';
213 foreach ($pages as $page) {
214 array_push($attachments, ...$page->attachments);
215 array_push($images, ...$page->images);
218 if (count($pages) > 0) {
220 if (!userCan('page-create', $parent)) {
221 $errors[] = 'You are lacking the required permission to create pages.';
224 $hasPermission = userCan('page-create-all') || userCan('page-create-own');
225 if (!$hasPermission) {
226 $errors[] = 'You are lacking the required permission to create pages.';
231 if (count($images) > 0) {
232 if (!userCan('image-create-all')) {
233 $errors[] = 'You are lacking the required permissions to create images.';
237 if (count($attachments) > 0) {
238 if (userCan('attachment-create-all')) {
239 $errors[] = 'You are lacking the required permissions to create attachments.';
243 if (count($errors)) {
244 throw new ZipImportException($errors);
248 protected function getZipPath(Import $import): string
250 if (!$this->storage->isRemote()) {
251 return $this->storage->getSystemPath($import->path);
254 $tempFilePath = tempnam(sys_get_temp_dir(), 'bszip-import-');
255 $tempFile = fopen($tempFilePath, 'wb');
256 $stream = $this->storage->getReadStream($import->path);
257 stream_copy_to_stream($stream, $tempFile);
260 return $tempFilePath;