]> BookStack Code Mirror - bookstack/blob - app/Exports/ZipExports/ZipImportRunner.php
345c22be153b6e928879a1366d1ec750ba081299
[bookstack] / app / Exports / ZipExports / ZipImportRunner.php
1 <?php
2
3 namespace BookStack\Exports\ZipExports;
4
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;
22
23 class ZipImportRunner
24 {
25     protected array $tempFilesToCleanup = []; // TODO
26
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,
34     ) {
35     }
36
37     /**
38      * @throws ZipImportException
39      */
40     public function run(Import $import, ?Entity $parent = null): void
41     {
42         $zipPath = $this->getZipPath($import);
43         $reader = new ZipExportReader($zipPath);
44
45         $errors = (new ZipExportValidator($reader))->validate();
46         if ($errors) {
47             throw new ZipImportException(["ZIP failed to validate"]);
48         }
49
50         try {
51             $exportModel = $reader->decodeDataToExportModel();
52         } catch (ZipExportException $e) {
53             throw new ZipImportException([$e->getMessage()]);
54         }
55
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"]);
63         }
64
65         $this->ensurePermissionsPermitImport($exportModel);
66
67         // TODO - Run import
68           // TODO - In transaction?
69             // TODO - Revert uploaded files if goes wrong
70               // TODO - Attachments
71               // TODO - Images
72               // (Both listed/stored in references)
73
74         $this->references->replaceReferences();
75     }
76
77     protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
78     {
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 ?? []),
84         ]);
85
86         // TODO - Parse/format description_html references
87
88         if ($book->cover) {
89             $this->references->addImage($book->cover, null);
90         }
91
92         // TODO - Pages
93         foreach ($exportBook->chapters as $exportChapter) {
94             $this->importChapter($exportChapter, $book, $reader);
95         }
96         // TODO - Sort chapters/pages by order
97
98         $this->references->addBook($book, $exportBook);
99
100         return $book;
101     }
102
103     protected function importChapter(ZipExportChapter $exportChapter, Book $parent, ZipExportReader $reader): Chapter
104     {
105         $chapter = $this->chapterRepo->create([
106             'name' => $exportChapter->name,
107             'description_html' => $exportChapter->description_html ?? '',
108             'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
109         ], $parent);
110
111         // TODO - Parse/format description_html references
112
113         $exportPages = $exportChapter->pages;
114         usort($exportPages, function (ZipExportPage $a, ZipExportPage $b) {
115             return ($a->priority ?? 0) - ($b->priority ?? 0);
116         });
117
118         foreach ($exportPages as $exportPage) {
119             //
120         }
121         // TODO - Pages
122
123         $this->references->addChapter($chapter, $exportChapter);
124
125         return $chapter;
126     }
127
128     protected function importPage(ZipExportPage $exportPage, Book|Chapter $parent, ZipExportReader $reader): Page
129     {
130         $page = $this->pageRepo->getNewDraftPage($parent);
131
132         // TODO - Import attachments
133           // TODO - Add attachment references
134         // TODO - Import images
135           // TODO - Add image references
136         // TODO - Parse/format HTML
137
138         $this->pageRepo->publishDraft($page, [
139             'name' => $exportPage->name,
140             'markdown' => $exportPage->markdown,
141             'html' => $exportPage->html,
142             'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
143         ]);
144
145         $this->references->addPage($page, $exportPage);
146
147         return $page;
148     }
149
150     protected function exportTagsToInputArray(array $exportTags): array
151     {
152         $tags = [];
153
154         /** @var ZipExportTag $tag */
155         foreach ($exportTags as $tag) {
156             $tags[] = ['name' => $tag->name, 'value' => $tag->value ?? ''];
157         }
158
159         return $tags;
160     }
161
162     protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile
163     {
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);
168         fclose($tempStream);
169
170         $this->tempFilesToCleanup[] = $tempPath;
171
172         return new UploadedFile($tempPath, $fileName);
173     }
174
175     /**
176      * @throws ZipImportException
177      */
178     protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|ZipExportBook $exportModel, Book|Chapter|null $parent = null): void
179     {
180         $errors = [];
181
182         // TODO - Extract messages to language files
183         // TODO - Ensure these are shown to users on failure
184
185         $chapters = [];
186         $pages = [];
187         $images = [];
188         $attachments = [];
189
190         if ($exportModel instanceof ZipExportBook) {
191             if (!userCan('book-create-all')) {
192                 $errors[] = 'You are lacking the required permission to create books.';
193             }
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;
200         }
201
202         foreach ($chapters as $chapter) {
203             array_push($pages, ...$chapter->pages);
204         }
205
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.';
210             }
211         }
212
213         foreach ($pages as $page) {
214             array_push($attachments, ...$page->attachments);
215             array_push($images, ...$page->images);
216         }
217
218         if (count($pages) > 0) {
219             if ($parent) {
220                 if (!userCan('page-create', $parent)) {
221                     $errors[] = 'You are lacking the required permission to create pages.';
222                 }
223             } else {
224                 $hasPermission = userCan('page-create-all') || userCan('page-create-own');
225                 if (!$hasPermission) {
226                     $errors[] = 'You are lacking the required permission to create pages.';
227                 }
228             }
229         }
230
231         if (count($images) > 0) {
232             if (!userCan('image-create-all')) {
233                 $errors[] = 'You are lacking the required permissions to create images.';
234             }
235         }
236
237         if (count($attachments) > 0) {
238             if (userCan('attachment-create-all')) {
239                 $errors[] = 'You are lacking the required permissions to create attachments.';
240             }
241         }
242
243         if (count($errors)) {
244             throw new ZipImportException($errors);
245         }
246     }
247
248     protected function getZipPath(Import $import): string
249     {
250         if (!$this->storage->isRemote()) {
251             return $this->storage->getSystemPath($import->path);
252         }
253
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);
258         fclose($tempFile);
259
260         return $tempFilePath;
261     }
262 }