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