3 namespace Tests\Uploads;
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;
13 class AttachmentTest extends TestCase
16 * Get a test file that can be uploaded.
18 protected function getTestFile(string $fileName): UploadedFile
20 return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', null, true);
24 * Uploads a file with the given name.
26 protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Testing\TestResponse
28 $file = $this->getTestFile($name);
30 return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
34 * Create a new attachment.
36 protected function createAttachment(Page $page): Attachment
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,
44 return Attachment::query()->latest()->first();
48 * Create a new upload attachment from the given data.
50 protected function createUploadAttachment(Page $page, string $filename, string $content, string $mimeType): Attachment
53 $filePath = stream_get_meta_data($file)['uri'];
54 file_put_contents($filePath, $content);
55 $upload = new UploadedFile($filePath, $filename, $mimeType, null, true);
57 $this->call('POST', '/attachments/upload', ['uploaded_to' => $page->id], [], ['file' => $upload], []);
59 return $page->attachments()->latest()->firstOrFail();
63 * Delete all uploaded files.
64 * To assist with cleanup.
66 protected function deleteUploads()
68 $fileService = $this->app->make(AttachmentService::class);
69 foreach (Attachment::all() as $file) {
70 $fileService->deleteFile($file);
74 public function test_file_upload()
76 $page = Page::query()->first();
78 $admin = $this->getAdmin();
79 $fileName = 'upload_test_file.txt';
83 'uploaded_to'=> $page->id,
86 'created_by' => $admin->id,
87 'updated_by' => $admin->id,
90 $upload = $this->uploadFile($fileName, $page->id);
91 $upload->assertStatus(200);
93 $attachment = Attachment::query()->orderBy('id', 'desc')->first();
94 $upload->assertJson($expectedResp);
96 $expectedResp['path'] = $attachment->path;
97 $this->assertDatabaseHas('attachments', $expectedResp);
99 $this->deleteUploads();
102 public function test_file_upload_does_not_use_filename()
104 $page = Page::query()->first();
105 $fileName = 'upload_test_file.txt';
107 $upload = $this->asAdmin()->uploadFile($fileName, $page->id);
108 $upload->assertStatus(200);
110 $attachment = Attachment::query()->orderBy('id', 'desc')->first();
111 $this->assertStringNotContainsString($fileName, $attachment->path);
112 $this->assertStringEndsWith('-txt', $attachment->path);
113 $this->deleteUploads();
116 public function test_file_display_and_access()
118 $page = Page::query()->first();
120 $fileName = 'upload_test_file.txt';
122 $upload = $this->uploadFile($fileName, $page->id);
123 $upload->assertStatus(200);
124 $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
126 $pageGet = $this->get($page->getUrl());
127 $pageGet->assertSeeText($fileName);
128 $pageGet->assertSee($attachment->getUrl());
130 $attachmentGet = $this->get($attachment->getUrl());
131 $attachmentGet->assertSee('Hi, This is a test file for testing the upload process.');
133 $this->deleteUploads();
136 public function test_attaching_link_to_page()
138 $page = Page::query()->first();
139 $admin = $this->getAdmin();
142 $linkReq = $this->call('POST', 'attachments/link', [
143 'attachment_link_url' => 'https://example.com',
144 'attachment_link_name' => 'Example Attachment Link',
145 'attachment_link_uploaded_to' => $page->id,
149 'path' => 'https://example.com',
150 'name' => 'Example Attachment Link',
151 'uploaded_to' => $page->id,
152 'created_by' => $admin->id,
153 'updated_by' => $admin->id,
159 $linkReq->assertStatus(200);
160 $this->assertDatabaseHas('attachments', $expectedData);
161 $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
163 $pageGet = $this->get($page->getUrl());
164 $pageGet->assertSeeText('Example Attachment Link');
165 $pageGet->assertSee($attachment->getUrl());
167 $attachmentGet = $this->get($attachment->getUrl());
168 $attachmentGet->assertRedirect('https://example.com');
170 $this->deleteUploads();
173 public function test_attachment_updating()
175 $page = Page::query()->first();
178 $attachment = $this->createAttachment($page);
179 $update = $this->call('PUT', 'attachments/' . $attachment->id, [
180 'attachment_edit_name' => 'My new attachment name',
181 'attachment_edit_url' => 'https://test.example.com',
185 'id' => $attachment->id,
186 'path' => 'https://test.example.com',
187 'name' => 'My new attachment name',
188 'uploaded_to' => $page->id,
191 $update->assertStatus(200);
192 $this->assertDatabaseHas('attachments', $expectedData);
194 $this->deleteUploads();
197 public function test_file_deletion()
199 $page = Page::query()->first();
201 $fileName = 'deletion_test.txt';
202 $this->uploadFile($fileName, $page->id);
204 $attachment = Attachment::query()->orderBy('id', 'desc')->first();
205 $filePath = storage_path($attachment->path);
206 $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
208 $attachment = Attachment::first();
209 $this->delete($attachment->getUrl());
211 $this->assertDatabaseMissing('attachments', [
214 $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
216 $this->deleteUploads();
219 public function test_attachment_deletion_on_page_deletion()
221 $page = Page::query()->first();
223 $fileName = 'deletion_test.txt';
224 $this->uploadFile($fileName, $page->id);
226 $attachment = Attachment::query()->orderBy('id', 'desc')->first();
227 $filePath = storage_path($attachment->path);
229 $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
230 $this->assertDatabaseHas('attachments', [
234 app(PageRepo::class)->destroy($page);
235 app(TrashCan::class)->empty();
237 $this->assertDatabaseMissing('attachments', [
240 $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
242 $this->deleteUploads();
245 public function test_attachment_access_without_permission_shows_404()
247 $admin = $this->getAdmin();
248 $viewer = $this->getViewer();
249 $page = Page::query()->first(); /** @var Page $page */
250 $this->actingAs($admin);
251 $fileName = 'permission_test.txt';
252 $this->uploadFile($fileName, $page->id);
253 $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
255 $page->restricted = true;
256 $page->permissions()->delete();
258 $page->rebuildPermissions();
259 $page->load('jointPermissions');
261 $this->actingAs($viewer);
262 $attachmentGet = $this->get($attachment->getUrl());
263 $attachmentGet->assertStatus(404);
264 $attachmentGet->assertSee('Attachment not found');
266 $this->deleteUploads();
269 public function test_data_and_js_links_cannot_be_attached_to_a_page()
271 $page = Page::query()->first();
275 'javascript:alert("bunny")',
276 ' javascript:alert("bunny")',
277 'JavaScript:alert("bunny")',
278 "\t\n\t\nJavaScript:alert(\"bunny\")",
279 'data:text/html;<a></a>',
280 'Data:text/html;<a></a>',
281 'Data:text/html;<a></a>',
284 foreach ($badLinks as $badLink) {
285 $linkReq = $this->post('attachments/link', [
286 'attachment_link_url' => $badLink,
287 'attachment_link_name' => 'Example Attachment Link',
288 'attachment_link_uploaded_to' => $page->id,
290 $linkReq->assertStatus(422);
291 $this->assertDatabaseMissing('attachments', [
296 $attachment = $this->createAttachment($page);
298 foreach ($badLinks as $badLink) {
299 $linkReq = $this->put('attachments/' . $attachment->id, [
300 'attachment_edit_url' => $badLink,
301 'attachment_edit_name' => 'Example Attachment Link',
303 $linkReq->assertStatus(422);
304 $this->assertDatabaseMissing('attachments', [
310 public function test_file_access_with_open_query_param_provides_inline_response_with_correct_content_type()
312 $page = Page::query()->first();
314 $fileName = 'upload_test_file.txt';
316 $upload = $this->uploadFile($fileName, $page->id);
317 $upload->assertStatus(200);
318 $attachment = Attachment::query()->orderBy('id', 'desc')->take(1)->first();
320 $attachmentGet = $this->get($attachment->getUrl(true));
321 // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
322 $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
323 $attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
324 $attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');
326 $this->deleteUploads();
329 public function test_html_file_access_with_open_forces_plain_content_type()
331 $page = Page::query()->first();
334 $attachment = $this->createUploadAttachment($page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
336 $attachmentGet = $this->get($attachment->getUrl(true));
337 // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
338 $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
339 $attachmentGet->assertHeader('Content-Disposition', 'inline; filename="test_file.html"');
341 $this->deleteUploads();