]> BookStack Code Mirror - bookstack/blob - tests/Uploads/AttachmentTest.php
60fd370b676d0d592be5bfad9610ff04c0e55dcf
[bookstack] / tests / Uploads / AttachmentTest.php
1 <?php
2
3 namespace Tests\Uploads;
4
5 use BookStack\Entities\Models\Page;
6 use BookStack\Entities\Repos\PageRepo;
7 use BookStack\Entities\Tools\TrashCan;
8 use BookStack\Uploads\Attachment;
9 use BookStack\Uploads\AttachmentService;
10 use Illuminate\Http\UploadedFile;
11 use Tests\TestCase;
12
13 class AttachmentTest extends TestCase
14 {
15     /**
16      * Get a test file that can be uploaded.
17      */
18     protected function getTestFile(string $fileName): UploadedFile
19     {
20         return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
21     }
22
23     /**
24      * Uploads a file with the given name.
25      */
26     protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Foundation\Testing\TestResponse
27     {
28         $file = $this->getTestFile($name);
29
30         return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
31     }
32
33     /**
34      * Create a new attachment.
35      */
36     protected function createAttachment(Page $page): Attachment
37     {
38         $this->post('attachments/link', [
39             'attachment_link_url'         => 'https://example.com',
40             'attachment_link_name'        => 'Example Attachment Link',
41             'attachment_link_uploaded_to' => $page->id,
42         ]);
43
44         return Attachment::query()->latest()->first();
45     }
46
47     /**
48      * Delete all uploaded files.
49      * To assist with cleanup.
50      */
51     protected function deleteUploads()
52     {
53         $fileService = $this->app->make(AttachmentService::class);
54         foreach (Attachment::all() as $file) {
55             $fileService->deleteFile($file);
56         }
57     }
58
59     public function test_file_upload()
60     {
61         $page = Page::query()->first();
62         $this->asAdmin();
63         $admin = $this->getAdmin();
64         $fileName = 'upload_test_file.txt';
65
66         $expectedResp = [
67             'name'       => $fileName,
68             'uploaded_to'=> $page->id,
69             'extension'  => 'txt',
70             'order'      => 1,
71             'created_by' => $admin->id,
72             'updated_by' => $admin->id,
73         ];
74
75         $upload = $this->uploadFile($fileName, $page->id);
76         $upload->assertStatus(200);
77
78         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
79         $upload->assertJson($expectedResp);
80
81         $expectedResp['path'] = $attachment->path;
82         $this->assertDatabaseHas('attachments', $expectedResp);
83
84         $this->deleteUploads();
85     }
86
87     public function test_file_upload_does_not_use_filename()
88     {
89         $page = Page::query()->first();
90         $fileName = 'upload_test_file.txt';
91
92         $upload = $this->asAdmin()->uploadFile($fileName, $page->id);
93         $upload->assertStatus(200);
94
95         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
96         $this->assertStringNotContainsString($fileName, $attachment->path);
97         $this->assertStringEndsWith('.txt', $attachment->path);
98     }
99
100     public function test_file_display_and_access()
101     {
102         $page = Page::query()->first();
103         $this->asAdmin();
104         $fileName = 'upload_test_file.txt';
105
106         $upload = $this->uploadFile($fileName, $page->id);
107         $upload->assertStatus(200);
108         $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
109
110         $pageGet = $this->get($page->getUrl());
111         $pageGet->assertSeeText($fileName);
112         $pageGet->assertSee($attachment->getUrl());
113
114         $attachmentGet = $this->get($attachment->getUrl());
115         $attachmentGet->assertSee('Hi, This is a test file for testing the upload process.');
116
117         $this->deleteUploads();
118     }
119
120     public function test_attaching_link_to_page()
121     {
122         $page = Page::query()->first();
123         $admin = $this->getAdmin();
124         $this->asAdmin();
125
126         $linkReq = $this->call('POST', 'attachments/link', [
127             'attachment_link_url'         => 'https://example.com',
128             'attachment_link_name'        => 'Example Attachment Link',
129             'attachment_link_uploaded_to' => $page->id,
130         ]);
131
132         $expectedData = [
133             'path'        => 'https://example.com',
134             'name'        => 'Example Attachment Link',
135             'uploaded_to' => $page->id,
136             'created_by'  => $admin->id,
137             'updated_by'  => $admin->id,
138             'external'    => true,
139             'order'       => 1,
140             'extension'   => '',
141         ];
142
143         $linkReq->assertStatus(200);
144         $this->assertDatabaseHas('attachments', $expectedData);
145         $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
146
147         $pageGet = $this->get($page->getUrl());
148         $pageGet->assertSeeText('Example Attachment Link');
149         $pageGet->assertSee($attachment->getUrl());
150
151         $attachmentGet = $this->get($attachment->getUrl());
152         $attachmentGet->assertRedirect('https://example.com');
153
154         $this->deleteUploads();
155     }
156
157     public function test_attachment_updating()
158     {
159         $page = Page::query()->first();
160         $this->asAdmin();
161
162         $attachment = $this->createAttachment($page);
163         $update = $this->call('PUT', 'attachments/' . $attachment->id, [
164             'attachment_edit_name' => 'My new attachment name',
165             'attachment_edit_url'  => 'https://test.example.com',
166         ]);
167
168         $expectedData = [
169             'id'          => $attachment->id,
170             'path'        => 'https://test.example.com',
171             'name'        => 'My new attachment name',
172             'uploaded_to' => $page->id,
173         ];
174
175         $update->assertStatus(200);
176         $this->assertDatabaseHas('attachments', $expectedData);
177
178         $this->deleteUploads();
179     }
180
181     public function test_file_deletion()
182     {
183         $page = Page::query()->first();
184         $this->asAdmin();
185         $fileName = 'deletion_test.txt';
186         $this->uploadFile($fileName, $page->id);
187
188         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
189         $filePath = storage_path($attachment->path);
190         $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
191
192         $attachment = Attachment::first();
193         $this->delete($attachment->getUrl());
194
195         $this->assertDatabaseMissing('attachments', [
196             'name' => $fileName,
197         ]);
198         $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
199
200         $this->deleteUploads();
201     }
202
203     public function test_attachment_deletion_on_page_deletion()
204     {
205         $page = Page::query()->first();
206         $this->asAdmin();
207         $fileName = 'deletion_test.txt';
208         $this->uploadFile($fileName, $page->id);
209
210         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
211         $filePath = storage_path($attachment->path);
212
213         $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
214         $this->assertDatabaseHas('attachments', [
215             'name' => $fileName,
216         ]);
217
218         app(PageRepo::class)->destroy($page);
219         app(TrashCan::class)->empty();
220
221         $this->assertDatabaseMissing('attachments', [
222             'name' => $fileName,
223         ]);
224         $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
225
226         $this->deleteUploads();
227     }
228
229     public function test_attachment_access_without_permission_shows_404()
230     {
231         $admin = $this->getAdmin();
232         $viewer = $this->getViewer();
233         $page = Page::query()->first(); /** @var Page $page */
234         $this->actingAs($admin);
235         $fileName = 'permission_test.txt';
236         $this->uploadFile($fileName, $page->id);
237         $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
238
239         $page->restricted = true;
240         $page->permissions()->delete();
241         $page->save();
242         $page->rebuildPermissions();
243         $page->load('jointPermissions');
244
245         $this->actingAs($viewer);
246         $attachmentGet = $this->get($attachment->getUrl());
247         $attachmentGet->assertStatus(404);
248         $attachmentGet->assertSee('Attachment not found');
249
250         $this->deleteUploads();
251     }
252
253     public function test_data_and_js_links_cannot_be_attached_to_a_page()
254     {
255         $page = Page::query()->first();
256         $this->asAdmin();
257
258         $badLinks = [
259             'javascript:alert("bunny")',
260             ' javascript:alert("bunny")',
261             'JavaScript:alert("bunny")',
262             "\t\n\t\nJavaScript:alert(\"bunny\")",
263             'data:text/html;<a></a>',
264             'Data:text/html;<a></a>',
265             'Data:text/html;<a></a>',
266         ];
267
268         foreach ($badLinks as $badLink) {
269             $linkReq = $this->post('attachments/link', [
270                 'attachment_link_url'         => $badLink,
271                 'attachment_link_name'        => 'Example Attachment Link',
272                 'attachment_link_uploaded_to' => $page->id,
273             ]);
274             $linkReq->assertStatus(422);
275             $this->assertDatabaseMissing('attachments', [
276                 'path' => $badLink,
277             ]);
278         }
279
280         $attachment = $this->createAttachment($page);
281
282         foreach ($badLinks as $badLink) {
283             $linkReq = $this->put('attachments/' . $attachment->id, [
284                 'attachment_edit_url'  => $badLink,
285                 'attachment_edit_name' => 'Example Attachment Link',
286             ]);
287             $linkReq->assertStatus(422);
288             $this->assertDatabaseMissing('attachments', [
289                 'path' => $badLink,
290             ]);
291         }
292     }
293
294     public function test_file_access_with_open_query_param_provides_inline_response_with_correct_content_type()
295     {
296         $page = Page::query()->first();
297         $this->asAdmin();
298         $fileName = 'upload_test_file.txt';
299
300         $upload = $this->uploadFile($fileName, $page->id);
301         $upload->assertStatus(200);
302         $attachment = Attachment::query()->orderBy('id', 'desc')->take(1)->first();
303
304         $attachmentGet = $this->get($attachment->getUrl(true));
305         // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
306         $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
307         $attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
308
309         $this->deleteUploads();
310     }
311 }