]> BookStack Code Mirror - bookstack/blob - tests/Exports/ZipExportTest.php
ac07b33aef5f7c5567ea3afb2f8ff7938937a5b2
[bookstack] / tests / Exports / ZipExportTest.php
1 <?php
2
3 namespace Tests\Exports;
4
5 use BookStack\Activity\Models\Tag;
6 use BookStack\Entities\Repos\BookRepo;
7 use BookStack\Entities\Tools\PageContent;
8 use BookStack\Uploads\Attachment;
9 use BookStack\Uploads\Image;
10 use Illuminate\Support\Carbon;
11 use Illuminate\Testing\TestResponse;
12 use Tests\TestCase;
13 use ZipArchive;
14
15 class ZipExportTest extends TestCase
16 {
17     public function test_export_results_in_zip_format()
18     {
19         $page = $this->entities->page();
20         $response = $this->asEditor()->get($page->getUrl("/export/zip"));
21
22         $zipData = $response->streamedContent();
23         $zipFile = tempnam(sys_get_temp_dir(), 'bstesta-');
24         file_put_contents($zipFile, $zipData);
25         $zip = new ZipArchive();
26         $zip->open($zipFile, ZipArchive::RDONLY);
27
28         $this->assertNotFalse($zip->locateName('data.json'));
29         $this->assertNotFalse($zip->locateName('files/'));
30
31         $data = json_decode($zip->getFromName('data.json'), true);
32         $this->assertIsArray($data);
33         $this->assertGreaterThan(0, count($data));
34
35         $zip->close();
36         unlink($zipFile);
37     }
38
39     public function test_export_metadata()
40     {
41         $page = $this->entities->page();
42         $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
43         $zip = $this->extractZipResponse($zipResp);
44
45         $this->assertEquals($page->id, $zip->data['page']['id'] ?? null);
46         $this->assertArrayNotHasKey('book', $zip->data);
47         $this->assertArrayNotHasKey('chapter', $zip->data);
48
49         $now = time();
50         $date = Carbon::parse($zip->data['exported_at'])->unix();
51         $this->assertLessThan($now + 2, $date);
52         $this->assertGreaterThan($now - 2, $date);
53
54         $version = trim(file_get_contents(base_path('version')));
55         $this->assertEquals($version, $zip->data['instance']['version']);
56
57         $instanceId = decrypt($zip->data['instance']['id_ciphertext']);
58         $this->assertEquals('bookstack', $instanceId);
59     }
60
61     public function test_page_export()
62     {
63         $page = $this->entities->page();
64         $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
65         $zip = $this->extractZipResponse($zipResp);
66
67         $pageData = $zip->data['page'];
68         $this->assertEquals([
69             'id' => $page->id,
70             'name' => $page->name,
71             'html' => (new PageContent($page))->render(),
72             'priority' => $page->priority,
73             'attachments' => [],
74             'images' => [],
75             'tags' => [],
76         ], $pageData);
77     }
78
79     public function test_page_export_with_markdown()
80     {
81         $page = $this->entities->page();
82         $markdown = "# My page\n\nwritten in markdown for export\n";
83         $page->markdown = $markdown;
84         $page->save();
85
86         $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
87         $zip = $this->extractZipResponse($zipResp);
88
89         $pageData = $zip->data['page'];
90         $this->assertEquals($markdown, $pageData['markdown']);
91         $this->assertNotEmpty($pageData['html']);
92     }
93
94     public function test_page_export_with_tags()
95     {
96         $page = $this->entities->page();
97         $page->tags()->saveMany([
98             new Tag(['name' => 'Exporty', 'value' => 'Content', 'order' => 1]),
99             new Tag(['name' => 'Another', 'value' => '', 'order' => 2]),
100         ]);
101
102         $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
103         $zip = $this->extractZipResponse($zipResp);
104
105         $pageData = $zip->data['page'];
106         $this->assertEquals([
107             [
108                 'name' => 'Exporty',
109                 'value' => 'Content',
110                 'order' => 1,
111             ],
112             [
113                 'name' => 'Another',
114                 'value' => '',
115                 'order' => 2,
116             ]
117         ], $pageData['tags']);
118     }
119
120     public function test_page_export_with_images()
121     {
122         $this->asEditor();
123         $page = $this->entities->page();
124         $result = $this->files->uploadGalleryImageToPage($this, $page);
125         $displayThumb = $result['response']->thumbs->gallery ?? '';
126         $page->html = '<p><img src="' . $displayThumb . '" alt="My image"></p>';
127         $page->save();
128         $image = Image::findOrFail($result['response']->id);
129
130         $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
131         $zip = $this->extractZipResponse($zipResp);
132         $pageData = $zip->data['page'];
133
134         $this->assertCount(1, $pageData['images']);
135         $imageData = $pageData['images'][0];
136         $this->assertEquals($image->id, $imageData['id']);
137         $this->assertEquals($image->name, $imageData['name']);
138         $this->assertEquals('gallery', $imageData['type']);
139         $this->assertNotEmpty($imageData['file']);
140
141         $filePath = $zip->extractPath("files/{$imageData['file']}");
142         $this->assertFileExists($filePath);
143         $this->assertEquals(file_get_contents(public_path($image->path)), file_get_contents($filePath));
144
145         $this->assertEquals('<p><img src="[[bsexport:image:' . $imageData['id'] . ']]" alt="My image"></p>', $pageData['html']);
146     }
147
148     public function test_page_export_file_attachments()
149     {
150         $contents = 'My great attachment content!';
151
152         $page = $this->entities->page();
153         $this->asAdmin();
154         $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'PageAttachmentExport.txt', $contents, 'text/plain');
155
156         $zipResp = $this->get($page->getUrl("/export/zip"));
157         $zip = $this->extractZipResponse($zipResp);
158
159         $pageData = $zip->data['page'];
160         $this->assertCount(1, $pageData['attachments']);
161
162         $attachmentData = $pageData['attachments'][0];
163         $this->assertEquals('PageAttachmentExport.txt', $attachmentData['name']);
164         $this->assertEquals($attachment->id, $attachmentData['id']);
165         $this->assertEquals(1, $attachmentData['order']);
166         $this->assertArrayNotHasKey('link', $attachmentData);
167         $this->assertNotEmpty($attachmentData['file']);
168
169         $fileRef = $attachmentData['file'];
170         $filePath = $zip->extractPath("/files/$fileRef");
171         $this->assertFileExists($filePath);
172         $this->assertEquals($contents, file_get_contents($filePath));
173     }
174
175     public function test_page_export_link_attachments()
176     {
177         $page = $this->entities->page();
178         $this->asEditor();
179         $attachment = Attachment::factory()->create([
180             'name' => 'My link attachment for export',
181             'path' => 'https://example.com/cats',
182             'external' => true,
183             'uploaded_to' => $page->id,
184             'order' => 1,
185         ]);
186
187         $zipResp = $this->get($page->getUrl("/export/zip"));
188         $zip = $this->extractZipResponse($zipResp);
189
190         $pageData = $zip->data['page'];
191         $this->assertCount(1, $pageData['attachments']);
192
193         $attachmentData = $pageData['attachments'][0];
194         $this->assertEquals('My link attachment for export', $attachmentData['name']);
195         $this->assertEquals($attachment->id, $attachmentData['id']);
196         $this->assertEquals(1, $attachmentData['order']);
197         $this->assertEquals('https://example.com/cats', $attachmentData['link']);
198         $this->assertArrayNotHasKey('file', $attachmentData);
199     }
200
201     public function test_book_export()
202     {
203         $book = $this->entities->book();
204         $book->tags()->saveMany(Tag::factory()->count(2)->make());
205
206         $zipResp = $this->asEditor()->get($book->getUrl("/export/zip"));
207         $zip = $this->extractZipResponse($zipResp);
208         $this->assertArrayHasKey('book', $zip->data);
209
210         $bookData = $zip->data['book'];
211         $this->assertEquals($book->id, $bookData['id']);
212         $this->assertEquals($book->name, $bookData['name']);
213         $this->assertEquals($book->descriptionHtml(), $bookData['description_html']);
214         $this->assertCount(2, $bookData['tags']);
215         $this->assertCount($book->directPages()->count(), $bookData['pages']);
216         $this->assertCount($book->chapters()->count(), $bookData['chapters']);
217         $this->assertArrayNotHasKey('cover', $bookData);
218     }
219
220     public function test_book_export_with_cover_image()
221     {
222         $book = $this->entities->book();
223         $bookRepo = $this->app->make(BookRepo::class);
224         $coverImageFile = $this->files->uploadedImage('cover.png');
225         $bookRepo->updateCoverImage($book, $coverImageFile);
226         $coverImage = $book->cover()->first();
227
228         $zipResp = $this->asEditor()->get($book->getUrl("/export/zip"));
229         $zip = $this->extractZipResponse($zipResp);
230
231         $this->assertArrayHasKey('cover', $zip->data['book']);
232         $coverRef = $zip->data['book']['cover'];
233         $coverPath = $zip->extractPath("/files/$coverRef");
234         $this->assertFileExists($coverPath);
235         $this->assertEquals(file_get_contents(public_path($coverImage->path)), file_get_contents($coverPath));
236     }
237
238     public function test_chapter_export()
239     {
240         $chapter = $this->entities->chapter();
241         $chapter->tags()->saveMany(Tag::factory()->count(2)->make());
242
243         $zipResp = $this->asEditor()->get($chapter->getUrl("/export/zip"));
244         $zip = $this->extractZipResponse($zipResp);
245         $this->assertArrayHasKey('chapter', $zip->data);
246
247         $chapterData = $zip->data['chapter'];
248         $this->assertEquals($chapter->id, $chapterData['id']);
249         $this->assertEquals($chapter->name, $chapterData['name']);
250         $this->assertEquals($chapter->descriptionHtml(), $chapterData['description_html']);
251         $this->assertCount(2, $chapterData['tags']);
252         $this->assertEquals($chapter->priority, $chapterData['priority']);
253         $this->assertCount($chapter->pages()->count(), $chapterData['pages']);
254     }
255
256
257     public function test_cross_reference_links_are_converted()
258     {
259         $book = $this->entities->bookHasChaptersAndPages();
260         $chapter = $book->chapters()->first();
261         $page = $chapter->pages()->first();
262
263         $book->description_html = '<p><a href="' . $chapter->getUrl() . '">Link to chapter</a></p>';
264         $book->save();
265         $chapter->description_html = '<p><a href="' . $page->getUrl() . '#section2">Link to page</a></p>';
266         $chapter->save();
267         $page->html = '<p><a href="' . $book->getUrl() . '?view=true">Link to book</a></p>';
268         $page->save();
269
270         $zipResp = $this->asEditor()->get($book->getUrl("/export/zip"));
271         $zip = $this->extractZipResponse($zipResp);
272         $bookData = $zip->data['book'];
273         $chapterData = $bookData['chapters'][0];
274         $pageData = $chapterData['pages'][0];
275
276         $this->assertStringContainsString('href="[[bsexport:chapter:' . $chapter->id . ']]"', $bookData['description_html']);
277         $this->assertStringContainsString('href="[[bsexport:page:' . $page->id . ']]#section2"', $chapterData['description_html']);
278         $this->assertStringContainsString('href="[[bsexport:book:' . $book->id . ']]?view=true"', $pageData['html']);
279     }
280
281     public function test_cross_reference_links_external_to_export_are_not_converted()
282     {
283         $page = $this->entities->page();
284         $page->html = '<p><a href="' . $page->book->getUrl() . '">Link to book</a></p>';
285         $page->save();
286
287         $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
288         $zip = $this->extractZipResponse($zipResp);
289         $pageData = $zip->data['page'];
290
291         $this->assertStringContainsString('href="' . $page->book->getUrl() . '"', $pageData['html']);
292     }
293
294     public function test_attachments_links_are_converted()
295     {
296         $page = $this->entities->page();
297         $attachment = Attachment::factory()->create([
298             'name' => 'My link attachment for export reference',
299             'path' => 'https://example.com/cats/ref',
300             'external' => true,
301             'uploaded_to' => $page->id,
302             'order' => 1,
303         ]);
304
305         $page->html = '<p><a href="' . url("/attachments/{$attachment->id}") . '?open=true">Link to attachment</a></p>';
306         $page->save();
307
308         $zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
309         $zip = $this->extractZipResponse($zipResp);
310         $pageData = $zip->data['page'];
311
312         $this->assertStringContainsString('href="[[bsexport:attachment:' . $attachment->id . ']]?open=true"', $pageData['html']);
313     }
314
315     public function test_links_in_markdown_are_parsed()
316     {
317         $chapter = $this->entities->chapterHasPages();
318         $page = $chapter->pages()->first();
319
320         $page->markdown = "[Link to chapter]({$chapter->getUrl()})";
321         $page->save();
322
323         $zipResp = $this->asEditor()->get($chapter->getUrl("/export/zip"));
324         $zip = $this->extractZipResponse($zipResp);
325         $pageData = $zip->data['chapter']['pages'][0];
326
327         $this->assertStringContainsString("[Link to chapter]([[bsexport:chapter:{$chapter->id}]])", $pageData['markdown']);
328     }
329
330     protected function extractZipResponse(TestResponse $response): ZipResultData
331     {
332         $zipData = $response->streamedContent();
333         $zipFile = tempnam(sys_get_temp_dir(), 'bstest-');
334
335         file_put_contents($zipFile, $zipData);
336         $extractDir = tempnam(sys_get_temp_dir(), 'bstestextracted-');
337         if (file_exists($extractDir)) {
338             unlink($extractDir);
339         }
340         mkdir($extractDir);
341
342         $zip = new ZipArchive();
343         $zip->open($zipFile, ZipArchive::RDONLY);
344         $zip->extractTo($extractDir);
345
346         $dataJson = file_get_contents($extractDir . DIRECTORY_SEPARATOR . "data.json");
347         $data = json_decode($dataJson, true);
348
349         return new ZipResultData(
350             $zipFile,
351             $extractDir,
352             $data,
353         );
354     }
355 }