]> BookStack Code Mirror - bookstack/blob - app/Exports/ZipExports/ZipImportRunner.php
ZIP imports: Started actual import logic
[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     protected array $createdImages = []; // TODO
27     protected array $createdAttachments = []; // TODO
28
29     public function __construct(
30         protected FileStorage $storage,
31         protected PageRepo $pageRepo,
32         protected ChapterRepo $chapterRepo,
33         protected BookRepo $bookRepo,
34         protected ImageService $imageService,
35     ) {
36     }
37
38     /**
39      * @throws ZipImportException
40      */
41     public function run(Import $import, ?Entity $parent = null): void
42     {
43         $zipPath = $this->getZipPath($import);
44         $reader = new ZipExportReader($zipPath);
45
46         $errors = (new ZipExportValidator($reader))->validate();
47         if ($errors) {
48             throw new ZipImportException(["ZIP failed to validate"]);
49         }
50
51         try {
52             $exportModel = $reader->decodeDataToExportModel();
53         } catch (ZipExportException $e) {
54             throw new ZipImportException([$e->getMessage()]);
55         }
56
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"]);
64         }
65
66         $this->ensurePermissionsPermitImport($exportModel);
67
68         // TODO - Run import
69           // TODO - In transaction?
70             // TODO - Revert uploaded files if goes wrong
71     }
72
73     protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader): Book
74     {
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 ?? []),
80         ]);
81
82         // TODO - Parse/format description_html references
83
84         if ($book->cover) {
85             $this->createdImages[] = $book->cover;
86         }
87
88         // TODO - Pages
89         foreach ($exportBook->chapters as $exportChapter) {
90             $this->importChapter($exportChapter, $book);
91         }
92         // TODO - Sort chapters/pages by order
93
94         return $book;
95     }
96
97     protected function importChapter(ZipExportChapter $exportChapter, Book $parent, ZipExportReader $reader): Chapter
98     {
99         $chapter = $this->chapterRepo->create([
100             'name' => $exportChapter->name,
101             'description_html' => $exportChapter->description_html ?? '',
102             'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []),
103         ], $parent);
104
105         // TODO - Parse/format description_html references
106
107         $exportPages = $exportChapter->pages;
108         usort($exportPages, function (ZipExportPage $a, ZipExportPage $b) {
109             return ($a->priority ?? 0) - ($b->priority ?? 0);
110         });
111
112         foreach ($exportPages as $exportPage) {
113             //
114         }
115         // TODO - Pages
116
117         return $chapter;
118     }
119
120     protected function importPage(ZipExportPage $exportPage, Book|Chapter $parent, ZipExportReader $reader): Page
121     {
122         $page = $this->pageRepo->getNewDraftPage($parent);
123
124         // TODO - Import attachments
125         // TODO - Import images
126         // TODO - Parse/format HTML
127
128         $this->pageRepo->publishDraft($page, [
129             'name' => $exportPage->name,
130             'markdown' => $exportPage->markdown,
131             'html' => $exportPage->html,
132             'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []),
133         ]);
134
135         return $page;
136     }
137
138     protected function exportTagsToInputArray(array $exportTags): array
139     {
140         $tags = [];
141
142         /** @var ZipExportTag $tag */
143         foreach ($exportTags as $tag) {
144             $tags[] = ['name' => $tag->name, 'value' => $tag->value ?? ''];
145         }
146
147         return $tags;
148     }
149
150     protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile
151     {
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);
156         fclose($tempStream);
157
158         $this->tempFilesToCleanup[] = $tempPath;
159
160         return new UploadedFile($tempPath, $fileName);
161     }
162
163     /**
164      * @throws ZipImportException
165      */
166     protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|ZipExportBook $exportModel, Book|Chapter|null $parent = null): void
167     {
168         $errors = [];
169
170         // TODO - Extract messages to language files
171         // TODO - Ensure these are shown to users on failure
172
173         $chapters = [];
174         $pages = [];
175         $images = [];
176         $attachments = [];
177
178         if ($exportModel instanceof ZipExportBook) {
179             if (!userCan('book-create-all')) {
180                 $errors[] = 'You are lacking the required permission to create books.';
181             }
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;
188         }
189
190         foreach ($chapters as $chapter) {
191             array_push($pages, ...$chapter->pages);
192         }
193
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.';
198             }
199         }
200
201         foreach ($pages as $page) {
202             array_push($attachments, ...$page->attachments);
203             array_push($images, ...$page->images);
204         }
205
206         if (count($pages) > 0) {
207             if ($parent) {
208                 if (!userCan('page-create', $parent)) {
209                     $errors[] = 'You are lacking the required permission to create pages.';
210                 }
211             } else {
212                 $hasPermission = userCan('page-create-all') || userCan('page-create-own');
213                 if (!$hasPermission) {
214                     $errors[] = 'You are lacking the required permission to create pages.';
215                 }
216             }
217         }
218
219         if (count($images) > 0) {
220             if (!userCan('image-create-all')) {
221                 $errors[] = 'You are lacking the required permissions to create images.';
222             }
223         }
224
225         if (count($attachments) > 0) {
226             if (userCan('attachment-create-all')) {
227                 $errors[] = 'You are lacking the required permissions to create attachments.';
228             }
229         }
230
231         if (count($errors)) {
232             throw new ZipImportException($errors);
233         }
234     }
235
236     protected function getZipPath(Import $import): string
237     {
238         if (!$this->storage->isRemote()) {
239             return $this->storage->getSystemPath($import->path);
240         }
241
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);
246         fclose($tempFile);
247
248         return $tempFilePath;
249     }
250 }