]> BookStack Code Mirror - bookstack/blobdiff - tests/Uploads/ImageTest.php
Guest create page: name field autofocus
[bookstack] / tests / Uploads / ImageTest.php
index 69b6dc90e96218296e84d51db5d788a7af5a7aa5..0e4065a827453c743188b16342f00d3bf5ddcdd2 100644 (file)
@@ -15,7 +15,7 @@ class ImageTest extends TestCase
 
     public function test_image_upload()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $admin = $this->getAdmin();
         $this->actingAs($admin);
 
@@ -39,7 +39,7 @@ class ImageTest extends TestCase
 
     public function test_image_display_thumbnail_generation_does_not_increase_image_size()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $admin = $this->getAdmin();
         $this->actingAs($admin);
 
@@ -61,6 +61,19 @@ class ImageTest extends TestCase
         $this->assertEquals($originalFileSize, $displayFileSize, 'Display thumbnail generation should not increase image size');
     }
 
+    public function test_image_display_thumbnail_generation_for_apng_images_uses_original_file()
+    {
+        $page = $this->entities->page();
+        $admin = $this->getAdmin();
+        $this->actingAs($admin);
+
+        $imgDetails = $this->uploadGalleryImage($page, 'animated.png');
+        $this->deleteImage($imgDetails['path']);
+
+        $this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery);
+        $this->assertStringNotContainsString('thumbs-', $imgDetails['response']->thumbs->display);
+    }
+
     public function test_image_edit()
     {
         $editor = $this->getEditor();
@@ -91,11 +104,13 @@ class ImageTest extends TestCase
 
         $pageId = $imgDetails['page']->id;
         $firstPageRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}");
-        $firstPageRequest->assertSuccessful()->assertElementExists('div');
+        $firstPageRequest->assertSuccessful();
+        $this->withHtml($firstPageRequest)->assertElementExists('div');
         $firstPageRequest->assertSuccessful()->assertSeeText($image->name);
 
         $secondPageRequest = $this->get("/images/gallery?page=2&uploaded_to={$pageId}");
-        $secondPageRequest->assertSuccessful()->assertElementNotExists('div');
+        $secondPageRequest->assertSuccessful();
+        $this->withHtml($secondPageRequest)->assertElementNotExists('div');
 
         $namePartial = substr($imgDetails['name'], 0, 3);
         $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
@@ -104,12 +119,13 @@ class ImageTest extends TestCase
         $namePartial = Str::random(16);
         $searchFailRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
         $searchFailRequest->assertSuccessful()->assertDontSee($imgDetails['name']);
-        $searchFailRequest->assertSuccessful()->assertElementNotExists('div');
+        $searchFailRequest->assertSuccessful();
+        $this->withHtml($searchFailRequest)->assertElementNotExists('div');
     }
 
     public function test_image_usage()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $editor = $this->getEditor();
         $this->actingAs($editor);
 
@@ -129,7 +145,7 @@ class ImageTest extends TestCase
 
     public function test_php_files_cannot_be_uploaded()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $admin = $this->getAdmin();
         $this->actingAs($admin);
 
@@ -151,7 +167,7 @@ class ImageTest extends TestCase
 
     public function test_php_like_files_cannot_be_uploaded()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $admin = $this->getAdmin();
         $this->actingAs($admin);
 
@@ -168,7 +184,7 @@ class ImageTest extends TestCase
 
     public function test_files_with_double_extensions_will_get_sanitized()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $admin = $this->getAdmin();
         $this->actingAs($admin);
 
@@ -203,7 +219,7 @@ class ImageTest extends TestCase
         ];
         foreach ($badNames as $name) {
             $galleryFile = $this->getTestImage($name);
-            $page = Page::query()->first();
+            $page = $this->entities->page();
             $badPath = $this->getTestImagePath('gallery', $name);
             $this->deleteImage($badPath);
 
@@ -228,7 +244,7 @@ class ImageTest extends TestCase
         config()->set('filesystems.images', 'local_secure');
         $this->asEditor();
         $galleryFile = $this->getTestImage('my-secure-test-upload.png');
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
 
         $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
@@ -241,12 +257,42 @@ class ImageTest extends TestCase
         }
     }
 
+    public function test_secure_image_paths_traversal_causes_500()
+    {
+        config()->set('filesystems.images', 'local_secure');
+        $this->asEditor();
+
+        $resp = $this->get('/uploads/images/../../logs/laravel.log');
+        $resp->assertStatus(500);
+    }
+
+    public function test_secure_image_paths_traversal_on_non_secure_images_causes_404()
+    {
+        config()->set('filesystems.images', 'local');
+        $this->asEditor();
+
+        $resp = $this->get('/uploads/images/../../logs/laravel.log');
+        $resp->assertStatus(404);
+    }
+
+    public function test_secure_image_paths_dont_serve_non_images()
+    {
+        config()->set('filesystems.images', 'local_secure');
+        $this->asEditor();
+
+        $testFilePath = storage_path('/uploads/images/testing.txt');
+        file_put_contents($testFilePath, 'hello from test_secure_image_paths_dont_serve_non_images');
+
+        $resp = $this->get('/uploads/images/testing.txt');
+        $resp->assertStatus(404);
+    }
+
     public function test_secure_images_included_in_exports()
     {
         config()->set('filesystems.images', 'local_secure');
         $this->asEditor();
         $galleryFile = $this->getTestImage('my-secure-test-upload.png');
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
 
         $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
@@ -264,15 +310,32 @@ class ImageTest extends TestCase
         }
     }
 
-    public function test_system_images_remain_public()
+    public function test_system_images_remain_public_with_local_secure()
     {
         config()->set('filesystems.images', 'local_secure');
         $this->asAdmin();
         $galleryFile = $this->getTestImage('my-system-test-upload.png');
         $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-upload.png');
 
-        $upload = $this->call('POST', '/settings', [], [], ['app_logo' => $galleryFile], []);
-        $upload->assertRedirect('/settings');
+        $upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);
+        $upload->assertRedirect('/settings/customization');
+
+        $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);
+
+        if (file_exists($expectedPath)) {
+            unlink($expectedPath);
+        }
+    }
+
+    public function test_system_images_remain_public_with_local_secure_restricted()
+    {
+        config()->set('filesystems.images', 'local_secure_restricted');
+        $this->asAdmin();
+        $galleryFile = $this->getTestImage('my-system-test-restricted-upload.png');
+        $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-restricted-upload.png');
+
+        $upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);
+        $upload->assertRedirect('/settings/customization');
 
         $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);
 
@@ -281,9 +344,90 @@ class ImageTest extends TestCase
         }
     }
 
+    public function test_secure_restricted_images_inaccessible_without_relation_permission()
+    {
+        config()->set('filesystems.images', 'local_secure_restricted');
+        $this->asEditor();
+        $galleryFile = $this->getTestImage('my-secure-restricted-test-upload.png');
+        $page = $this->entities->page();
+
+        $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
+        $upload->assertStatus(200);
+        $expectedUrl = url('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-test-upload.png');
+        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-test-upload.png');
+
+        $this->get($expectedUrl)->assertOk();
+
+        $this->entities->setPermissions($page, [], []);
+
+        $resp = $this->get($expectedUrl);
+        $resp->assertNotFound();
+
+        if (file_exists($expectedPath)) {
+            unlink($expectedPath);
+        }
+    }
+
+    public function test_thumbnail_path_handled_by_secure_restricted_images()
+    {
+        config()->set('filesystems.images', 'local_secure_restricted');
+        $this->asEditor();
+        $galleryFile = $this->getTestImage('my-secure-restricted-thumb-test-test.png');
+        $page = $this->entities->page();
+
+        $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
+        $upload->assertStatus(200);
+        $expectedUrl = url('uploads/images/gallery/' . date('Y-m') . '/thumbs-150-150/my-secure-restricted-thumb-test-test.png');
+        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-thumb-test-test.png');
+
+        $this->get($expectedUrl)->assertOk();
+
+        $this->entities->setPermissions($page, [], []);
+
+        $resp = $this->get($expectedUrl);
+        $resp->assertNotFound();
+
+        if (file_exists($expectedPath)) {
+            unlink($expectedPath);
+        }
+    }
+
+    public function test_secure_restricted_image_access_controlled_in_exports()
+    {
+        config()->set('filesystems.images', 'local_secure_restricted');
+        $this->asEditor();
+        $galleryFile = $this->getTestImage('my-secure-restricted-export-test.png');
+
+        /** @var Page $pageA */
+        /** @var Page $pageB */
+        $pageA = Page::query()->first();
+        $pageB = Page::query()->where('id', '!=', $pageA->id)->first();
+        $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-restricted-export-test.png');
+
+        $upload = $this->asEditor()->call('POST', '/images/gallery', ['uploaded_to' => $pageA->id], [], ['file' => $galleryFile], []);
+        $upload->assertOk();
+
+        $imageUrl = json_decode($upload->getContent(), true)['url'];
+        $pageB->html .= "<img src=\"{$imageUrl}\">";
+        $pageB->save();
+
+        $encodedImageContent = base64_encode(file_get_contents($expectedPath));
+        $export = $this->get($pageB->getUrl('/export/html'));
+        $this->assertStringContainsString($encodedImageContent, $export->getContent());
+
+        $this->entities->setPermissions($pageA, [], []);
+
+        $export = $this->get($pageB->getUrl('/export/html'));
+        $this->assertStringNotContainsString($encodedImageContent, $export->getContent());
+
+        if (file_exists($expectedPath)) {
+            unlink($expectedPath);
+        }
+    }
+
     public function test_image_delete()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $this->asAdmin();
         $imageName = 'first-image.png';
         $relPath = $this->getTestImagePath('gallery', $imageName);
@@ -305,7 +449,7 @@ class ImageTest extends TestCase
 
     public function test_image_delete_does_not_delete_similar_images()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $this->asAdmin();
         $imageName = 'first-image.png';
 
@@ -328,6 +472,32 @@ class ImageTest extends TestCase
         $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected');
     }
 
+    public function test_image_manager_delete_button_only_shows_with_permission()
+    {
+        $page = $this->entities->page();
+        $this->asAdmin();
+        $imageName = 'first-image.png';
+        $relPath = $this->getTestImagePath('gallery', $imageName);
+        $this->deleteImage($relPath);
+        $viewer = $this->getViewer();
+
+        $this->uploadImage($imageName, $page->id);
+        $image = Image::first();
+
+        $resp = $this->get("/images/edit/{$image->id}");
+        $this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]');
+
+        $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
+        $this->withHtml($resp)->assertElementNotExists('button#image-manager-delete[title="Delete"]');
+
+        $this->giveUserPermissions($viewer, ['image-delete-all']);
+
+        $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
+        $this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]');
+
+        $this->deleteImage($relPath);
+    }
+
     protected function getTestProfileImage()
     {
         $imageName = 'profile.png';
@@ -384,7 +554,7 @@ class ImageTest extends TestCase
 
     public function test_deleted_unused_images()
     {
-        $page = Page::query()->first();
+        $page = $this->entities->page();
         $admin = $this->getAdmin();
         $this->actingAs($admin);