use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Attachment extends Model
{
use HasCreatorAndUpdater;
+ use HasFactory;
protected $fillable = ['name', 'order'];
protected $hidden = ['path', 'page'];
--- /dev/null
+<?php
+
+namespace Database\Factories\Uploads;
+
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Page;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Uploads\Attachment>
+ */
+class AttachmentFactory extends Factory
+{
+ /**
+ * The name of the factory's corresponding model.
+ *
+ * @var string
+ */
+ protected $model = \BookStack\Uploads\Attachment::class;
+
+ /**
+ * Define the model's default state.
+ *
+ * @return array<string, mixed>
+ */
+ public function definition()
+ {
+ return [
+ 'name' => $this->faker->words(2, true),
+ 'path' => $this->faker->url(),
+ 'extension' => '',
+ 'external' => true,
+ 'uploaded_to' => Page::factory(),
+ 'created_by' => User::factory(),
+ 'updated_by' => User::factory(),
+ 'order' => 0,
+ ];
+ }
+}
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
-use Tests\Uploads\UsesImages;
class BooksApiTest extends TestCase
{
use TestsApi;
- use UsesImages;
protected string $baseEndpoint = '/api/books';
/** @var Book $book */
$book = $this->entities->book();
$this->assertNull($book->cover);
- $file = $this->getTestImage('image.png');
+ $file = $this->files->uploadedImage('image.png');
// Ensure cover image can be set via API
$resp = $this->call('PUT', $this->baseEndpoint . "/{$book->id}", [
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
-use Tests\Uploads\UsesImages;
class ShelvesApiTest extends TestCase
{
use TestsApi;
- use UsesImages;
protected string $baseEndpoint = '/api/shelves';
/** @var Book $shelf */
$shelf = Bookshelf::visible()->first();
$this->assertNull($shelf->cover);
- $file = $this->getTestImage('image.png');
+ $file = $this->files->uploadedImage('image.png');
// Ensure cover image can be set via API
$resp = $this->call('PUT', $this->baseEndpoint . "/{$shelf->id}", [
use BookStack\Uploads\Image;
use Illuminate\Support\Str;
use Tests\TestCase;
-use Tests\Uploads\UsesImages;
class BookShelfTest extends TestCase
{
- use UsesImages;
-
public function test_shelves_shows_in_header_if_have_view_permissions()
{
$viewer = $this->users->viewer();
'description' => 'Test book description ' . Str::random(10),
];
- $imageFile = $this->getTestImage('shelf-test.png');
+ $imageFile = $this->files->uploadedImage('shelf-test.png');
$resp = $this->asEditor()->call('POST', '/shelves', $shelfInfo, [], ['image' => $imageFile]);
$resp->assertRedirect();
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookRepo;
use Tests\TestCase;
-use Tests\Uploads\UsesImages;
class BookTest extends TestCase
{
- use UsesImages;
-
public function test_create()
{
$book = Book::factory()->make([
{
$book = $this->entities->book();
$bookRepo = $this->app->make(BookRepo::class);
- $coverImageFile = $this->getTestImage('cover.png');
+ $coverImageFile = $this->files->uploadedImage('cover.png');
$bookRepo->updateCoverImage($book, $coverImageFile);
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\PageContent;
use Tests\TestCase;
-use Tests\Uploads\UsesImages;
class PageContentTest extends TestCase
{
- use UsesImages;
-
protected $base64Jpeg = '/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=';
public function test_page_includes()
$imageFile = public_path($imagePath);
$this->assertEquals(base64_decode($this->base64Jpeg), file_get_contents($imageFile));
- $this->deleteImage($imagePath);
+ $this->files->deleteAtRelativePath($imagePath);
}
public function test_base64_images_get_extracted_when_containing_whitespace()
$imageFile = public_path($imagePath);
$this->assertEquals(base64_decode($base64PngWithoutWhitespace), file_get_contents($imageFile));
- $this->deleteImage($imagePath);
+ $this->files->deleteAtRelativePath($imagePath);
}
public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract()
$imageFile = public_path($imagePath);
$this->assertEquals(base64_decode($this->base64Jpeg), file_get_contents($imageFile));
- $this->deleteImage($imagePath);
+ $this->files->deleteAtRelativePath($imagePath);
}
public function test_markdown_base64_extract_not_limited_by_pcre_limits()
$imageFile = public_path($imagePath);
$this->assertEquals($content, file_get_contents($imageFile));
- $this->deleteImage($imagePath);
+ $this->files->deleteAtRelativePath($imagePath);
ini_set('pcre.backtrack_limit', $pcreBacktrackLimit);
ini_set('pcre.recursion_limit', $pcreRecursionLimit);
}
--- /dev/null
+<?php
+
+namespace Tests\Helpers;
+
+use BookStack\Entities\Models\Page;
+use BookStack\Uploads\Attachment;
+use BookStack\Uploads\AttachmentService;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Testing\TestResponse;
+use stdClass;
+use Tests\TestCase;
+
+class FileProvider
+{
+ /**
+ * Get the path to a file in the test-data-directory.
+ */
+ public function testFilePath(string $fileName): string
+ {
+ return base_path('tests/test-data/' . $fileName);
+ }
+
+ /**
+ * Creates a new temporary image file using the given name,
+ * with the content decoded from the given bas64 file name.
+ * Is generally used for testing sketchy files that could trip AV.
+ */
+ public function imageFromBase64File(string $base64FileName, string $imageFileName): UploadedFile
+ {
+ $imagePath = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), $imageFileName]);
+ $base64FilePath = $this->testFilePath($base64FileName);
+ $data = file_get_contents($base64FilePath);
+ $decoded = base64_decode($data);
+ file_put_contents($imagePath, $decoded);
+
+ return new UploadedFile($imagePath, $imageFileName, 'image/png', null, true);
+ }
+
+ /**
+ * Get a test image UploadedFile instance, that can be uploaded via test requests.
+ */
+ public function uploadedImage(string $fileName, string $testDataFileName = ''): UploadedFile
+ {
+ return new UploadedFile($this->testFilePath($testDataFileName ?: 'test-image.png'), $fileName, 'image/png', null, true);
+ }
+
+ /**
+ * Get a test txt UploadedFile instance, that can be uploaded via test requests.
+ */
+ public function uploadedTextFile(string $fileName): UploadedFile
+ {
+ return new UploadedFile($this->testFilePath('test-file.txt'), $fileName, 'text/plain', null, true);
+ }
+
+ /**
+ * Get raw data for a PNG image test file.
+ */
+ public function pngImageData(): string
+ {
+ return file_get_contents($this->testFilePath('test-image.png'));
+ }
+
+ /**
+ * Get the expected relative path for an uploaded image of the given type and filename.
+ */
+ public function expectedImagePath(string $imageType, string $fileName): string
+ {
+ return '/uploads/images/' . $imageType . '/' . date('Y-m') . '/' . $fileName;
+ }
+
+ /**
+ * Performs an image gallery upload request with the given name.
+ */
+ public function uploadGalleryImage(TestCase $case, string $name, int $uploadedTo = 0, string $contentType = 'image/png', string $testDataFileName = ''): TestResponse
+ {
+ $file = $this->uploadedImage($name, $testDataFileName);
+
+ return $case->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], ['CONTENT_TYPE' => $contentType]);
+ }
+
+ /**
+ * Upload a new gallery image and return a set of details about the image,
+ * including the json decoded response of the upload.
+ * Ensures the upload succeeds.
+ *
+ * @return array{name: string, path: string, page: Page, response: stdClass}
+ */
+ public function uploadGalleryImageToPage(TestCase $case, Page $page, string $testDataFileName = ''): array
+ {
+ $imageName = $testDataFileName ?: 'first-image.png';
+ $relPath = $this->expectedImagePath('gallery', $imageName);
+ $this->deleteAtRelativePath($relPath);
+
+ $upload = $this->uploadGalleryImage($case, $imageName, $page->id, 'image/png', $testDataFileName);
+ $upload->assertStatus(200);
+
+ return [
+ 'name' => $imageName,
+ 'path' => $relPath,
+ 'page' => $page,
+ 'response' => json_decode($upload->getContent()),
+ ];
+ }
+
+ /**
+ * Uploads an attachment file with the given name.
+ */
+ public function uploadAttachmentFile(TestCase $case, string $name, int $uploadedTo = 0): TestResponse
+ {
+ $file = $this->uploadedTextFile($name);
+
+ return $case->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
+ }
+
+ /**
+ * Upload a new attachment from the given raw data of the given type, to the given page.
+ * Returns the attachment
+ */
+ public function uploadAttachmentDataToPage(TestCase $case, Page $page, string $filename, string $content, string $mimeType): Attachment
+ {
+ $file = tmpfile();
+ $filePath = stream_get_meta_data($file)['uri'];
+ file_put_contents($filePath, $content);
+ $upload = new UploadedFile($filePath, $filename, $mimeType, null, true);
+
+ $case->call('POST', '/attachments/upload', ['uploaded_to' => $page->id], [], ['file' => $upload], []);
+
+ return $page->attachments()->where('uploaded_to', '=', $page->id)->latest()->firstOrFail();
+ }
+
+ /**
+ * Delete an uploaded image.
+ */
+ public function deleteAtRelativePath(string $path): void
+ {
+ $fullPath = public_path($path);
+ if (file_exists($fullPath)) {
+ unlink($fullPath);
+ }
+ }
+
+ /**
+ * Delete all uploaded files.
+ * To assist with cleanup.
+ */
+ public function deleteAllAttachmentFiles(): void
+ {
+ $fileService = app()->make(AttachmentService::class);
+ foreach (Attachment::all() as $file) {
+ $fileService->deleteFile($file);
+ }
+ }
+}
use BookStack\Entities\Repos\BookRepo;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
-use Tests\Uploads\UsesImages;
class OpenGraphTest extends TestCase
{
- use UsesImages;
-
public function test_page_tags()
{
$page = $this->entities->page();
// Test image set if image has cover image
$bookRepo = app(BookRepo::class);
- $bookRepo->updateCoverImage($book, $this->getTestImage('image.png'));
+ $bookRepo->updateCoverImage($book, $this->files->uploadedImage('image.png'));
$resp = $this->asEditor()->get($book->getUrl());
$tags = $this->getOpenGraphTags($resp);
// Test image set if image has cover image
$baseRepo = app(BaseRepo::class);
- $baseRepo->updateCoverImage($shelf, $this->getTestImage('image.png'));
+ $baseRepo->updateCoverImage($shelf, $this->files->uploadedImage('image.png'));
$resp = $this->asEditor()->get($shelf->getUrl());
$tags = $this->getOpenGraphTags($resp);
namespace Tests\Settings;
use Tests\TestCase;
-use Tests\Uploads\UsesImages;
class SettingsTest extends TestCase
{
- use UsesImages;
-
public function test_settings_endpoint_redirects_to_settings_view()
{
$resp = $this->asAdmin()->get('/settings');
public function test_updating_and_removing_app_icon()
{
$this->asAdmin();
- $galleryFile = $this->getTestImage('my-app-icon.png');
+ $galleryFile = $this->files->uploadedImage('my-app-icon.png');
$expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-app-icon.png');
$this->assertFalse(setting()->get('app-icon'));
use Psr\Http\Client\ClientInterface;
use Ssddanbrown\AssertHtml\TestsHtml;
use Tests\Helpers\EntityProvider;
+use Tests\Helpers\FileProvider;
use Tests\Helpers\PermissionsProvider;
use Tests\Helpers\TestServiceProvider;
use Tests\Helpers\UserRoleProvider;
protected EntityProvider $entities;
protected UserRoleProvider $users;
protected PermissionsProvider $permissions;
+ protected FileProvider $files;
protected function setUp(): void
{
$this->entities = new EntityProvider();
$this->users = new UserRoleProvider();
$this->permissions = new PermissionsProvider($this->users);
+ $this->files = new FileProvider();
parent::setUp();
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Uploads\Attachment;
-use BookStack\Uploads\AttachmentService;
-use Illuminate\Http\UploadedFile;
use Tests\TestCase;
class AttachmentTest extends TestCase
{
- /**
- * Get a test file that can be uploaded.
- */
- protected function getTestFile(string $fileName): UploadedFile
- {
- return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', null, true);
- }
-
- /**
- * Uploads a file with the given name.
- */
- protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Testing\TestResponse
- {
- $file = $this->getTestFile($name);
-
- return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
- }
-
- /**
- * Create a new attachment.
- */
- protected function createAttachment(Page $page): Attachment
- {
- $this->post('attachments/link', [
- 'attachment_link_url' => 'https://example.com',
- 'attachment_link_name' => 'Example Attachment Link',
- 'attachment_link_uploaded_to' => $page->id,
- ]);
-
- return Attachment::query()->latest()->first();
- }
-
- /**
- * Create a new upload attachment from the given data.
- */
- protected function createUploadAttachment(Page $page, string $filename, string $content, string $mimeType): Attachment
- {
- $file = tmpfile();
- $filePath = stream_get_meta_data($file)['uri'];
- file_put_contents($filePath, $content);
- $upload = new UploadedFile($filePath, $filename, $mimeType, null, true);
-
- $this->call('POST', '/attachments/upload', ['uploaded_to' => $page->id], [], ['file' => $upload], []);
-
- return $page->attachments()->latest()->firstOrFail();
- }
-
- /**
- * Delete all uploaded files.
- * To assist with cleanup.
- */
- protected function deleteUploads()
- {
- $fileService = $this->app->make(AttachmentService::class);
- foreach (Attachment::all() as $file) {
- $fileService->deleteFile($file);
- }
- }
-
public function test_file_upload()
{
$page = $this->entities->page();
'updated_by' => $admin->id,
];
- $upload = $this->uploadFile($fileName, $page->id);
+ $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
$expectedResp['path'] = $attachment->path;
$this->assertDatabaseHas('attachments', $expectedResp);
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_file_upload_does_not_use_filename()
$page = $this->entities->page();
$fileName = 'upload_test_file.txt';
- $upload = $this->asAdmin()->uploadFile($fileName, $page->id);
+ $this->asAdmin();
+ $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
$this->assertStringNotContainsString($fileName, $attachment->path);
$this->assertStringEndsWith('-txt', $attachment->path);
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_file_display_and_access()
$this->asAdmin();
$fileName = 'upload_test_file.txt';
- $upload = $this->uploadFile($fileName, $page->id);
+ $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
$content = $attachmentGet->streamedContent();
$this->assertStringContainsString('Hi, This is a test file for testing the upload process.', $content);
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_attaching_link_to_page()
$attachmentGet = $this->get($attachment->getUrl());
$attachmentGet->assertRedirect('https://example.com');
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_attachment_updating()
$page = $this->entities->page();
$this->asAdmin();
- $attachment = $this->createAttachment($page);
+ $attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
$update = $this->call('PUT', 'attachments/' . $attachment->id, [
'attachment_edit_name' => 'My new attachment name',
'attachment_edit_url' => 'https://test.example.com',
$update->assertStatus(200);
$this->assertDatabaseHas('attachments', $expectedData);
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_file_deletion()
$page = $this->entities->page();
$this->asAdmin();
$fileName = 'deletion_test.txt';
- $this->uploadFile($fileName, $page->id);
+ $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
$filePath = storage_path($attachment->path);
]);
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_attachment_deletion_on_page_deletion()
$page = $this->entities->page();
$this->asAdmin();
$fileName = 'deletion_test.txt';
- $this->uploadFile($fileName, $page->id);
+ $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
$filePath = storage_path($attachment->path);
]);
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_attachment_access_without_permission_shows_404()
$page = $this->entities->page(); /** @var Page $page */
$this->actingAs($admin);
$fileName = 'permission_test.txt';
- $this->uploadFile($fileName, $page->id);
+ $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
$this->permissions->setEntityPermissions($page, [], []);
$attachmentGet->assertStatus(404);
$attachmentGet->assertSee('Attachment not found');
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_data_and_js_links_cannot_be_attached_to_a_page()
]);
}
- $attachment = $this->createAttachment($page);
+ $attachment = Attachment::factory()->create(['uploaded_to' => $page->id]);
foreach ($badLinks as $badLink) {
$linkReq = $this->put('attachments/' . $attachment->id, [
$this->asAdmin();
$fileName = 'upload_test_file.txt';
- $upload = $this->uploadFile($fileName, $page->id);
+ $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->take(1)->first();
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
$attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_html_file_access_with_open_forces_plain_content_type()
$page = $this->entities->page();
$this->asAdmin();
- $attachment = $this->createUploadAttachment($page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
+ $attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
$attachmentGet = $this->get($attachment->getUrl(true));
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="test_file.html"');
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
public function test_file_upload_works_when_local_secure_restricted_is_in_use()
$page = $this->entities->page();
$fileName = 'upload_test_file.txt';
- $upload = $this->asAdmin()->uploadFile($fileName, $page->id);
+ $this->asAdmin();
+ $upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->where('uploaded_to', '=', $page->id)->first();
$this->assertFileExists(storage_path($attachment->path));
- $this->deleteUploads();
+ $this->files->deleteAllAttachmentFiles();
}
}
class AvatarTest extends TestCase
{
- use UsesImages;
-
protected function createUserRequest($user): User
{
$this->asAdmin()->post('/settings/users/create', [
$http->shouldReceive('fetch')
->once()->with($url)
- ->andReturn($this->getTestImageContent());
+ ->andReturn($this->files->pngImageData());
}
protected function deleteUserImage(User $user)
{
- $this->deleteImage($user->avatar->path);
+ $this->files->deleteAtRelativePath($user->avatar->path);
}
public function test_gravatar_fetched_on_user_create()
namespace Tests\Uploads;
-use BookStack\Entities\Models\Page;
use BookStack\Uploads\Image;
use Tests\TestCase;
class DrawioTest extends TestCase
{
- use UsesImages;
-
public function test_get_image_as_base64()
{
$page = $this->entities->page();
$this->asAdmin();
$imageName = 'first-image.png';
- $this->uploadImage($imageName, $page->id);
+ $this->files->uploadGalleryImage($this, $imageName, $page->id);
/** @var Image $image */
$image = Image::query()->first();
$image->type = 'drawio';
$this->asEditor();
$imageName = 'non-accessible-image.png';
- $this->uploadImage($imageName, $page->id);
+ $this->files->uploadGalleryImage($this, $imageName, $page->id);
/** @var Image $image */
$image = Image::query()->first();
$image->type = 'drawio';
$image = Image::where('type', '=', 'drawio')->first();
$this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: ' . public_path($image->path));
- $testImageData = file_get_contents($this->getTestImageFilePath());
+ $testImageData = $this->files->pngImageData();
$uploadedImageData = file_get_contents(public_path($image->path));
$this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected');
}
namespace Tests\Uploads;
-use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
class ImageTest extends TestCase
{
- use UsesImages;
-
public function test_image_upload()
{
$page = $this->entities->page();
$admin = $this->users->admin();
$this->actingAs($admin);
- $imgDetails = $this->uploadGalleryImage($page);
+ $imgDetails = $this->files->uploadGalleryImageToPage($this, $page);
$relPath = $imgDetails['path'];
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));
- $this->deleteImage($relPath);
+ $this->files->deleteAtRelativePath($relPath);
$this->assertDatabaseHas('images', [
'url' => $this->baseUrl . $relPath,
$admin = $this->users->admin();
$this->actingAs($admin);
- $originalFile = $this->getTestImageFilePath('compressed.png');
+ $originalFile = $this->files->testFilePath('compressed.png');
$originalFileSize = filesize($originalFile);
- $imgDetails = $this->uploadGalleryImage($page, 'compressed.png');
+ $imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'compressed.png');
$relPath = $imgDetails['path'];
$this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));
$displayImagePath = public_path($displayImageRelPath);
$displayFileSize = filesize($displayImagePath);
- $this->deleteImage($relPath);
- $this->deleteImage($displayImageRelPath);
+ $this->files->deleteAtRelativePath($relPath);
+ $this->files->deleteAtRelativePath($displayImageRelPath);
$this->assertEquals($originalFileSize, $displayFileSize, 'Display thumbnail generation should not increase image size');
}
$admin = $this->users->admin();
$this->actingAs($admin);
- $imgDetails = $this->uploadGalleryImage($page, 'animated.png');
- $this->deleteImage($imgDetails['path']);
+ $imgDetails = $this->files->uploadGalleryImageToPage($this, $page, 'animated.png');
+ $this->files->deleteAtRelativePath($imgDetails['path']);
$this->assertStringContainsString('thumbs-', $imgDetails['response']->thumbs->gallery);
$this->assertStringNotContainsString('thumbs-', $imgDetails['response']->thumbs->display);
$editor = $this->users->editor();
$this->actingAs($editor);
- $imgDetails = $this->uploadGalleryImage();
+ $imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page());
$image = Image::query()->first();
$newName = Str::random();
$update->assertSuccessful();
$update->assertSee($newName);
- $this->deleteImage($imgDetails['path']);
+ $this->files->deleteAtRelativePath($imgDetails['path']);
$this->assertDatabaseHas('images', [
'type' => 'gallery',
{
$this->asEditor();
- $imgDetails = $this->uploadGalleryImage();
+ $imgDetails = $this->files->uploadGalleryImageToPage($this, $this->entities->page());
$image = Image::query()->first();
$pageId = $imgDetails['page']->id;
$editor = $this->users->editor();
$this->actingAs($editor);
- $imgDetails = $this->uploadGalleryImage($page);
+ $imgDetails = $this->files->uploadGalleryImageToPage($this, $page);
$image = Image::query()->first();
$page->html = '<img src="' . $image->url . '">';
$usage->assertSeeText($page->name);
$usage->assertSee($page->getUrl());
- $this->deleteImage($imgDetails['path']);
+ $this->files->deleteAtRelativePath($imgDetails['path']);
}
public function test_php_files_cannot_be_uploaded()
$this->actingAs($admin);
$fileName = 'bad.php';
- $relPath = $this->getTestImagePath('gallery', $fileName);
- $this->deleteImage($relPath);
+ $relPath = $this->files->expectedImagePath('gallery', $fileName);
+ $this->files->deleteAtRelativePath($relPath);
- $file = $this->newTestImageFromBase64('bad-php.base64', $fileName);
+ $file = $this->files->imageFromBase64File('bad-php.base64', $fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
$this->actingAs($admin);
$fileName = 'bad.phtml';
- $relPath = $this->getTestImagePath('gallery', $fileName);
- $this->deleteImage($relPath);
+ $relPath = $this->files->expectedImagePath('gallery', $fileName);
+ $this->files->deleteAtRelativePath($relPath);
- $file = $this->newTestImageFromBase64('bad-phtml.base64', $fileName);
+ $file = $this->files->imageFromBase64File('bad-phtml.base64', $fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
$this->actingAs($admin);
$fileName = 'bad.phtml.png';
- $relPath = $this->getTestImagePath('gallery', $fileName);
+ $relPath = $this->files->expectedImagePath('gallery', $fileName);
$expectedRelPath = dirname($relPath) . '/bad-phtml.png';
- $this->deleteImage($expectedRelPath);
+ $this->files->deleteAtRelativePath($expectedRelPath);
- $file = $this->newTestImageFromBase64('bad-phtml-png.base64', $fileName);
+ $file = $this->files->imageFromBase64File('bad-phtml-png.base64', $fileName);
$upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(200);
$this->assertFileDoesNotExist(public_path($relPath), 'Uploaded image file name was not stripped of dots');
$this->assertFileExists(public_path($expectedRelPath));
- $this->deleteImage($lastImage->path);
+ $this->files->deleteAtRelativePath($lastImage->path);
}
public function test_url_entities_removed_from_filenames()
'#.png',
];
foreach ($badNames as $name) {
- $galleryFile = $this->getTestImage($name);
+ $galleryFile = $this->files->uploadedImage($name);
$page = $this->entities->page();
- $badPath = $this->getTestImagePath('gallery', $name);
- $this->deleteImage($badPath);
+ $badPath = $this->files->expectedImagePath('gallery', $name);
+ $this->files->deleteAtRelativePath($badPath);
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$upload->assertStatus(200);
$this->assertTrue(strlen($newFileName) > 0, 'File name was reduced to nothing');
- $this->deleteImage($lastImage->path);
+ $this->files->deleteAtRelativePath($lastImage->path);
}
}
{
config()->set('filesystems.images', 'local_secure');
$this->asEditor();
- $galleryFile = $this->getTestImage('my-secure-test-upload.png');
+ $galleryFile = $this->files->uploadedImage('my-secure-test-upload.png');
$page = $this->entities->page();
$expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
{
config()->set('filesystems.images', 'local_secure');
$this->asEditor();
- $galleryFile = $this->getTestImage('my-secure-test-upload.png');
+ $galleryFile = $this->files->uploadedImage('my-secure-test-upload.png');
$page = $this->entities->page();
$expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
{
config()->set('filesystems.images', 'local_secure');
$this->asAdmin();
- $galleryFile = $this->getTestImage('my-system-test-upload.png');
+ $galleryFile = $this->files->uploadedImage('my-system-test-upload.png');
$expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-upload.png');
$upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asAdmin();
- $galleryFile = $this->getTestImage('my-system-test-restricted-upload.png');
+ $galleryFile = $this->files->uploadedImage('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], []);
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asEditor();
- $galleryFile = $this->getTestImage('my-secure-restricted-test-upload.png');
+ $galleryFile = $this->files->uploadedImage('my-secure-restricted-test-upload.png');
$page = $this->entities->page();
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asEditor();
- $galleryFile = $this->getTestImage('my-secure-restricted-thumb-test-test.png');
+ $galleryFile = $this->files->uploadedImage('my-secure-restricted-thumb-test-test.png');
$page = $this->entities->page();
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
{
config()->set('filesystems.images', 'local_secure_restricted');
$this->asEditor();
- $galleryFile = $this->getTestImage('my-secure-restricted-export-test.png');
+ $galleryFile = $this->files->uploadedImage('my-secure-restricted-export-test.png');
- /** @var Page $pageA */
- /** @var Page $pageB */
- $pageA = Page::query()->first();
- $pageB = Page::query()->where('id', '!=', $pageA->id)->first();
+ $pageA = $this->entities->page();
+ $pageB = $this->entities->page();
$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], []);
$page = $this->entities->page();
$this->asAdmin();
$imageName = 'first-image.png';
- $relPath = $this->getTestImagePath('gallery', $imageName);
- $this->deleteImage($relPath);
+ $relPath = $this->files->expectedImagePath('gallery', $imageName);
+ $this->files->deleteAtRelativePath($relPath);
- $this->uploadImage($imageName, $page->id);
+ $this->files->uploadGalleryImage($this, $imageName, $page->id);
$image = Image::first();
$delete = $this->delete('/images/' . $image->id);
$this->asAdmin();
$imageName = 'first-image.png';
- $relPath = $this->getTestImagePath('gallery', $imageName);
- $this->deleteImage($relPath);
+ $relPath = $this->files->expectedImagePath('gallery', $imageName);
+ $this->files->deleteAtRelativePath($relPath);
- $this->uploadImage($imageName, $page->id);
- $this->uploadImage($imageName, $page->id);
- $this->uploadImage($imageName, $page->id);
+ $this->files->uploadGalleryImage($this, $imageName, $page->id);
+ $this->files->uploadGalleryImage($this, $imageName, $page->id);
+ $this->files->uploadGalleryImage($this, $imageName, $page->id);
$image = Image::first();
$folder = public_path(dirname($relPath));
$page = $this->entities->page();
$this->asAdmin();
$imageName = 'first-image.png';
- $relPath = $this->getTestImagePath('gallery', $imageName);
- $this->deleteImage($relPath);
+ $relPath = $this->files->expectedImagePath('gallery', $imageName);
+ $this->files->deleteAtRelativePath($relPath);
$viewer = $this->users->viewer();
- $this->uploadImage($imageName, $page->id);
+ $this->files->uploadGalleryImage($this, $imageName, $page->id);
$image = Image::first();
$resp = $this->get("/images/edit/{$image->id}");
$resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
$this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]');
- $this->deleteImage($relPath);
+ $this->files->deleteAtRelativePath($relPath);
}
protected function getTestProfileImage()
{
$imageName = 'profile.png';
- $relPath = $this->getTestImagePath('user', $imageName);
- $this->deleteImage($relPath);
+ $relPath = $this->files->expectedImagePath('user', $imageName);
+ $this->files->deleteAtRelativePath($relPath);
- return $this->getTestImage($imageName);
+ return $this->files->uploadedImage($imageName);
}
public function test_user_image_upload()
$this->actingAs($admin);
$imageName = 'unused-image.png';
- $relPath = $this->getTestImagePath('gallery', $imageName);
- $this->deleteImage($relPath);
+ $relPath = $this->files->expectedImagePath('gallery', $imageName);
+ $this->files->deleteAtRelativePath($relPath);
- $upload = $this->uploadImage($imageName, $page->id);
+ $upload = $this->files->uploadGalleryImage($this, $imageName, $page->id);
$upload->assertStatus(200);
$image = Image::where('type', '=', 'gallery')->first();
$this->assertCount(1, $toDelete);
$this->assertFalse(file_exists($absPath));
- $this->deleteImage($relPath);
+ $this->files->deleteAtRelativePath($relPath);
}
}
+++ /dev/null
-<?php
-
-namespace Tests\Uploads;
-
-use BookStack\Entities\Models\Page;
-use Illuminate\Http\UploadedFile;
-use stdClass;
-
-trait UsesImages
-{
- /**
- * Get the path to a file in the test-data-directory.
- */
- protected function getTestImageFilePath(?string $fileName = null): string
- {
- if (is_null($fileName)) {
- $fileName = 'test-image.png';
- }
-
- return base_path('tests/test-data/' . $fileName);
- }
-
- /**
- * Creates a new temporary image file using the given name,
- * with the content decoded from the given bas64 file name.
- * Is generally used for testing sketchy files that could trip AV.
- */
- protected function newTestImageFromBase64(string $base64FileName, $imageFileName): UploadedFile
- {
- $imagePath = implode(DIRECTORY_SEPARATOR, [sys_get_temp_dir(), $imageFileName]);
- $base64FilePath = $this->getTestImageFilePath($base64FileName);
- $data = file_get_contents($base64FilePath);
- $decoded = base64_decode($data);
- file_put_contents($imagePath, $decoded);
-
- return new UploadedFile($imagePath, $imageFileName, 'image/png', null, true);
- }
-
- /**
- * Get a test image that can be uploaded.
- */
- protected function getTestImage(string $fileName, ?string $testDataFileName = null): UploadedFile
- {
- return new UploadedFile($this->getTestImageFilePath($testDataFileName), $fileName, 'image/png', null, true);
- }
-
- /**
- * Get the raw file data for the test image.
- *
- * @return false|string
- */
- protected function getTestImageContent()
- {
- return file_get_contents($this->getTestImageFilePath());
- }
-
- /**
- * Get the path for a test image.
- */
- protected function getTestImagePath(string $type, string $fileName): string
- {
- return '/uploads/images/' . $type . '/' . date('Y-m') . '/' . $fileName;
- }
-
- /**
- * Uploads an image with the given name.
- *
- * @param $name
- * @param int $uploadedTo
- * @param string $contentType
- *
- * @return \Illuminate\Foundation\Testing\TestResponse
- */
- protected function uploadImage($name, $uploadedTo = 0, $contentType = 'image/png', ?string $testDataFileName = null)
- {
- $file = $this->getTestImage($name, $testDataFileName);
-
- return $this->withHeader('Content-Type', $contentType)
- ->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
- }
-
- /**
- * Upload a new gallery image.
- * Returns the image name.
- * Can provide a page to relate the image to.
- *
- * @param Page|null $page
- *
- * @return array{name: string, path: string, page: Page, response: stdClass}
- */
- protected function uploadGalleryImage(Page $page = null, ?string $testDataFileName = null)
- {
- if ($page === null) {
- $page = $this->entities->page();
- }
-
- $imageName = $testDataFileName ?? 'first-image.png';
- $relPath = $this->getTestImagePath('gallery', $imageName);
- $this->deleteImage($relPath);
-
- $upload = $this->uploadImage($imageName, $page->id, 'image/png', $testDataFileName);
- $upload->assertStatus(200);
-
- return [
- 'name' => $imageName,
- 'path' => $relPath,
- 'page' => $page,
- 'response' => json_decode($upload->getContent()),
- ];
- }
-
- /**
- * Delete an uploaded image.
- */
- protected function deleteImage(string $relPath)
- {
- $path = public_path($relPath);
- if (file_exists($path)) {
- unlink($path);
- }
- }
-}
use Mockery\MockInterface;
use RuntimeException;
use Tests\TestCase;
-use Tests\Uploads\UsesImages;
class UserManagementTest extends TestCase
{
- use UsesImages;
-
public function test_user_creation()
{
/** @var User $user */
public function test_user_avatar_update_and_reset()
{
$user = $this->users->viewer();
- $avatarFile = $this->getTestImage('avatar-icon.png');
+ $avatarFile = $this->files->uploadedImage('avatar-icon.png');
$this->assertEquals(0, $user->image_id);