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
26 protected array $createdImages = []; // TODO
27 protected array $createdAttachments = []; // TODO
29 public function __construct(
30 protected FileStorage $storage,
31 protected PageRepo $pageRepo,
32 protected ChapterRepo $chapterRepo,
33 protected BookRepo $bookRepo,
34 protected ImageService $imageService,
39 * @throws ZipImportException
41 public function run(Import $import, ?Entity $parent = null): void
43 $zipPath = $this->getZipPath($import);
44 $reader = new ZipExportReader($zipPath);
46 $errors = (new ZipExportValidator($reader))->validate();
48 throw new ZipImportException(["ZIP failed to validate"]);
52 $exportModel = $reader->decodeDataToExportModel();
53 } catch (ZipExportException $e) {
54 throw new ZipImportException([$e->getMessage()]);
57 // Validate parent type
58 if ($exportModel instanceof ZipExportBook && ($parent !== null)) {
59 throw new ZipImportException(["Must not have a parent set for a Book import"]);
60 } else if ($exportModel instanceof ZipExportChapter && (!$parent instanceof Book)) {
61 throw new ZipImportException(["Parent book required for chapter import"]);
62 } else if ($exportModel instanceof ZipExportPage && !($parent instanceof Book || $parent instanceof Chapter)) {
63 throw new ZipImportException(["Parent book or chapter required for page import"]);
66 $this->ensurePermissionsPermitImport($exportModel);
69 // TODO - In transaction?
70 // TODO - Revert uploaded files if goes wrong
73 protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
75 $book = $this->bookRepo->create([
76 'name' => $exportBook->name,
77 'description_html' => $exportBook->description_html ?? '',
78 'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null,
79 'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []),
82 // TODO - Parse/format description_html references
85 $this->createdImages[] = $book->cover;
89 foreach ($exportBook->chapters as $exportChapter) {
90 $this->importChapter($exportChapter, $book);
92 // TODO - Sort chapters/pages by order
97 protected function importChapter(ZipExportChapter $exportChapter, Book $parent, ZipExportReader $reader): Chapter
99 $chapter = $this->chapterRepo->create([
100 'name' => $exportChapter->name,
101 'description_html' => $exportChapter->description_html ?? '',
102 'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
105 // TODO - Parse/format description_html references
107 $exportPages = $exportChapter->pages;
108 usort($exportPages, function (ZipExportPage $a, ZipExportPage $b) {
109 return ($a->priority ?? 0) - ($b->priority ?? 0);
112 foreach ($exportPages as $exportPage) {
120 protected function importPage(ZipExportPage $exportPage, Book|Chapter $parent, ZipExportReader $reader): Page
122 $page = $this->pageRepo->getNewDraftPage($parent);
124 // TODO - Import attachments
125 // TODO - Import images
126 // TODO - Parse/format HTML
128 $this->pageRepo->publishDraft($page, [
129 'name' => $exportPage->name,
130 'markdown' => $exportPage->markdown,
131 'html' => $exportPage->html,
132 'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
138 protected function exportTagsToInputArray(array $exportTags): array
142 /** @var ZipExportTag $tag */
143 foreach ($exportTags as $tag) {
144 $tags[] = ['name' => $tag->name, 'value' => $tag->value ?? ''];
150 protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile
152 $tempPath = tempnam(sys_get_temp_dir(), 'bszipextract');
153 $fileStream = $reader->streamFile($fileName);
154 $tempStream = fopen($tempPath, 'wb');
155 stream_copy_to_stream($fileStream, $tempStream);
158 $this->tempFilesToCleanup[] = $tempPath;
160 return new UploadedFile($tempPath, $fileName);
164 * @throws ZipImportException
166 protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|ZipExportBook $exportModel, Book|Chapter|null $parent = null): void
170 // TODO - Extract messages to language files
171 // TODO - Ensure these are shown to users on failure
178 if ($exportModel instanceof ZipExportBook) {
179 if (!userCan('book-create-all')) {
180 $errors[] = 'You are lacking the required permission to create books.';
182 array_push($pages, ...$exportModel->pages);
183 array_push($chapters, ...$exportModel->chapters);
184 } else if ($exportModel instanceof ZipExportChapter) {
185 $chapters[] = $exportModel;
186 } else if ($exportModel instanceof ZipExportPage) {
187 $pages[] = $exportModel;
190 foreach ($chapters as $chapter) {
191 array_push($pages, ...$chapter->pages);
194 if (count($chapters) > 0) {
195 $permission = 'chapter-create' . ($parent ? '' : '-all');
196 if (!userCan($permission, $parent)) {
197 $errors[] = 'You are lacking the required permission to create chapters.';
201 foreach ($pages as $page) {
202 array_push($attachments, ...$page->attachments);
203 array_push($images, ...$page->images);
206 if (count($pages) > 0) {
208 if (!userCan('page-create', $parent)) {
209 $errors[] = 'You are lacking the required permission to create pages.';
212 $hasPermission = userCan('page-create-all') || userCan('page-create-own');
213 if (!$hasPermission) {
214 $errors[] = 'You are lacking the required permission to create pages.';
219 if (count($images) > 0) {
220 if (!userCan('image-create-all')) {
221 $errors[] = 'You are lacking the required permissions to create images.';
225 if (count($attachments) > 0) {
226 if (userCan('attachment-create-all')) {
227 $errors[] = 'You are lacking the required permissions to create attachments.';
231 if (count($errors)) {
232 throw new ZipImportException($errors);
236 protected function getZipPath(Import $import): string
238 if (!$this->storage->isRemote()) {
239 return $this->storage->getSystemPath($import->path);
242 $tempFilePath = tempnam(sys_get_temp_dir(), 'bszip-import-');
243 $tempFile = fopen($tempFilePath, 'wb');
244 $stream = $this->storage->getReadStream($import->path);
245 stream_copy_to_stream($stream, $tempFile);
248 return $tempFilePath;