From e19c42ce235f730c05c5659b32f72b9b62f91851 Mon Sep 17 00:00:00 2001 From: Roelof Roos Date: Sun, 17 Jul 2022 01:51:30 +0200 Subject: [PATCH 1/4] Improved image and URL metadata handling - URLs no longer need to be alive, just valid - Use Laravel's HTTP library, not file_get_contents and streams - Added a bunch of tests - Replaced URL-image-upload mimetype test with response test --- composer.json | 11 +- .../EditorJsImageUploadController.php | 92 +++++---- .../Controllers/EditorJsLinkController.php | 55 ++--- .../EditorJsImageUploadControllerTest.php | 191 ++++++++++++++++++ .../EditorJsLinkControllerTest.php | 62 ++++++ tests/helpers.php | 25 +++ tests/resources/responses/image.gif | Bin 0 -> 8521 bytes tests/resources/responses/image.jpg | Bin 0 -> 4566 bytes tests/resources/responses/image.png | Bin 0 -> 9134 bytes tests/resources/responses/image.svg | 12 ++ tests/resources/responses/image.txt | 1 + tests/resources/responses/image.webp | Bin 0 -> 4238 bytes tests/resources/responses/simple.html | 13 ++ tests/resources/responses/with-image.html | 19 ++ 14 files changed, 415 insertions(+), 66 deletions(-) create mode 100644 tests/Unit/Http/Controllers/EditorJsImageUploadControllerTest.php create mode 100644 tests/Unit/Http/Controllers/EditorJsLinkControllerTest.php create mode 100644 tests/helpers.php create mode 100644 tests/resources/responses/image.gif create mode 100644 tests/resources/responses/image.jpg create mode 100644 tests/resources/responses/image.png create mode 100644 tests/resources/responses/image.svg create mode 100644 tests/resources/responses/image.txt create mode 100644 tests/resources/responses/image.webp create mode 100644 tests/resources/responses/simple.html create mode 100644 tests/resources/responses/with-image.html diff --git a/composer.json b/composer.json index 039b6b8..ce60969 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,16 @@ "autoload-dev": { "psr-4": { "Tests\\": "tests" - } + }, + "files": [ + "tests/helpers.php" + ] + }, + "scripts": { + "test": "phpunit" + }, + "scripts-descriptions": { + "test": "Test application using PHPUnit." }, "extra": { "laravel": { diff --git a/src/Http/Controllers/EditorJsImageUploadController.php b/src/Http/Controllers/EditorJsImageUploadController.php index f1e4eb9..35c19a0 100644 --- a/src/Http/Controllers/EditorJsImageUploadController.php +++ b/src/Http/Controllers/EditorJsImageUploadController.php @@ -1,32 +1,45 @@ all(), [ 'image' => 'required|image', ]); if ($validator->fails()) { - return [ - 'success' => 0 - ]; + return response()->json([ + 'success' => 0, + ]); } $path = $request->file('image')->store( @@ -51,60 +64,59 @@ public function file(NovaRequest $request) $thumbnails = $this->applyThumbnails($path); } - return [ + return response()->json([ 'success' => 1, 'file' => [ 'url' => Storage::disk(config('nova-editor-js.toolSettings.image.disk'))->url($path), 'thumbnails' => $thumbnails ] - ]; + ]); } /** - * @param NovaRequest $request - * @return array + * "Upload" a URL. */ - public function url(NovaRequest $request) + public function url(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ - 'url' => [ - 'required', - 'active_url', - function ($attribute, $value, $fail) { - $imageDetails = getimagesize($value); - - if (!in_array($imageDetails['mime'] ?? '', [ - 'image/jpeg', - 'image/webp', - 'image/gif', - 'image/png', - 'image/svg+xml', - ])) { - $fail($attribute . ' is invalid.'); - } - }, - ], + 'url' => 'required|url', ]); if ($validator->fails()) { - return [ - 'success' => 0 - ]; + return response()->json([ + 'success' => 0, + ]); } $url = $request->input('url'); - $imageContents = file_get_contents($url); - $name = parse_url(substr($url, strrpos($url, '/') + 1))['path']; - $nameWithPath = config('nova-editor-js.toolSettings.image.path') . '/' . uniqid() . $name; - Storage::disk(config('nova-editor-js.toolSettings.image.disk'))->put($nameWithPath, $imageContents); + // Fetch URL + try { + $response = Http::timeout(5)->get($url)->throw(); + } catch (ConnectionException | RequestException) { + return response()->json([ + 'success' => 0, + ]); + } + + // Validate mime type + $mime = (new finfo())->buffer($response->body(), FILEINFO_MIME_TYPE); + if (! in_array($mime, self::VALID_IMAGE_MIMES, true)) { + return response()->json([ + 'success' => 0, + ]); + } - return [ + $urlBasename = basename(parse_url(url($url), PHP_URL_PATH)); + $nameWithPath = config('nova-editor-js.toolSettings.image.path') . '/' . uniqid() . $urlBasename; + Storage::disk(config('nova-editor-js.toolSettings.image.disk'))->put($nameWithPath, $response->body()); + + return response()->json([ 'success' => 1, 'file' => [ 'url' => Storage::disk(config('nova-editor-js.toolSettings.image.disk'))->url($nameWithPath) ] - ]; + ]); } /** diff --git a/src/Http/Controllers/EditorJsLinkController.php b/src/Http/Controllers/EditorJsLinkController.php index bf4faf5..7938d37 100644 --- a/src/Http/Controllers/EditorJsLinkController.php +++ b/src/Http/Controllers/EditorJsLinkController.php @@ -1,35 +1,47 @@ all(), [ - 'url' => 'required|active_url', + 'url' => 'required|url', ]); if ($validator->fails()) { - return [ - 'success' => 0 - ]; + return response()->json([ + 'success' => 0, + ]); } - $contents = file_get_contents($request->input('url')); + // Contents + try { + $url = $request->input('url'); + $response = Http::timeout(5)->get($url)->throw(); + } catch (ConnectionException | RequestException) { + return response()->json([ + 'success' => 0, + ]); + } - $doc = new \DOMDocument(); - @$doc->loadHTML($contents); + $doc = new DOMDocument(); + @$doc->loadHTML((string) $response->getBody()); $nodes = $doc->getElementsByTagName('title'); $title = $nodes->item(0)->nodeValue; $description = ''; @@ -48,20 +60,13 @@ public function fetch(NovaRequest $request) } } - $results = [ + return response()->json([ 'success' => 1, - 'meta' => [ - 'title' => $title, + 'meta' => array_filter([ + 'title' => $title ?? $url, 'description' => $description, - ] - ]; - - if (!empty($imageUrl)){ - $results['meta']['image'] = [ - 'url' => $imageUrl, - ]; - } - - return $results; + 'imageUrl' => $imageUrl, + ]), + ]); } } diff --git a/tests/Unit/Http/Controllers/EditorJsImageUploadControllerTest.php b/tests/Unit/Http/Controllers/EditorJsImageUploadControllerTest.php new file mode 100644 index 0000000..7bd4429 --- /dev/null +++ b/tests/Unit/Http/Controllers/EditorJsImageUploadControllerTest.php @@ -0,0 +1,191 @@ +afterApplicationCreated(function () { + Route::post('/test/image/file', [EditorJsImageUploadController::class, 'file']); + Route::post('/test/image/url', [EditorJsImageUploadController::class, 'url']); + }); + } + + /** + * Test an image upload. + * @param string $path Path to the image file + * @dataProvider provideValidFilesForImageUpload + */ + public function test_image_upload(string $path): void + { + Storage::fake(); + Storage::fake('public'); + + $uploadedFile = UploadedFile::fake()->create('file', 1024, (new finfo())->file($path, FILEINFO_MIME_TYPE)); + if ($fp = $uploadedFile->openFile('w')) { + $fp->fwrite(file_get_contents($path)); + } + + $response = $this->post('/test/image/file', [ + 'image' => $uploadedFile, + ])->assertOk()->assertJson(['success' => 1]); + + $responseUrl = $response->json('file.url'); + $this->assertNotEmpty($responseUrl, 'Response file URL is empty'); + + $storageBaseUrl = Storage::disk('public')->url(''); + $this->assertStringStartsWith($storageBaseUrl, $responseUrl, 'Response URL seems to not be in a public folder'); + + $createdFiles = Storage::disk()->allFiles(); + $this->assertCount(2, $createdFiles, 'Storage seems to not contain exactly two files (one upload, one saved)'); + + $filesThatLookLikeTheUpload = array_filter( + $createdFiles, + fn ($file) => Str::endsWith($file, basename($responseUrl)), + ); + + $this->assertCount(1, $filesThatLookLikeTheUpload, 'Storage doesn\'t seem to contain a file with the same name as the returned URL'); + } + + /** + * Test uploading a non-image. + */ + public function test_non_image_upload(): void + { + Storage::fake(); + Storage::fake('public'); + + $uploadedFile = UploadedFile::fake()->createWithContent('upload', 'Hello World!'); + + $response = $this->post('/test/image/file', [ + 'image' => $uploadedFile, + ])->assertOk()->assertJson(['success' => 0]); + } + + /** + * Test submitting an image URL causes the file to be stored to disk and returned. + * + * @param string $file path to the file returned by the URL + * @dataProvider provideValidFiles + */ + public function test_valid_image_url_submission(string $file): void + { + Storage::fake(); + Storage::fake('public'); + + Http::fake([ + 'https://example.com/image.bin' => Http::response(file_get_contents($file)), + ])->preventStrayRequests(); + + $response = $this->post('/test/image/url', [ + 'url' => 'https://example.com/image.bin', + ])->assertOk()->assertJson(['success' => 1]); + + $responseUrl = $response->json('file.url'); + $this->assertNotEmpty($responseUrl, 'Response file URL is empty'); + + $storageBaseUrl = Storage::disk('public')->url(''); + $this->assertStringStartsWith($storageBaseUrl, $responseUrl, 'Response URL seems to not be in a public folder'); + + $createdFiles = Storage::disk()->allFiles(); + $this->assertCount(1, $createdFiles, 'Storage seems to not contain exactly one file'); + + $this->assertEquals(basename($createdFiles[0]), basename($responseUrl), 'Response URL filename doesn\'t match created file basename'); + } + + /** + * Test submitting a non-image URL causes the request to fail. + */ + public function test_invalid_image_url_submission(): void + { + Http::fake([ + 'https://example.com/image.bin' => Http::response('Hello World!'), + ])->preventStrayRequests(); + + $this->post('/test/image/url', [ + 'url' => 'https://example.com/image.bin', + ])->assertOk()->assertJson(['success' => 0]); + } + + /** + * Test submitting a URL that's not valid, but is a properly formed HTTP + * URL, still sends out a ping (but fails, eventually). + */ + public function test_submitting_a_dead_url(): void + { + Http::fake([ + 'https://example.invalid/image.bin' => Http::response('Hello World!'), + ])->preventStrayRequests(); + + $this->post('/test/image/url', [ + 'url' => 'https://example.invalid/image.bin', + ])->assertOk()->assertJson(['success' => 0]); + + Http::assertSentCount(1); + } + + /** + * Test submitting a URL which the server won't or cannot provide returns an error. + * Also implicitly handles timeouts, since that's the same block. + */ + public function test_submitting_image_url_with_errors(): void + { + Http::fake([ + 'https://example.com/client/image.bin' => Http::response(test_resource('responses/image.png'), Response::HTTP_BAD_GATEWAY), + 'https://example.com/server/image.bin' => Http::response(test_resource('responses/image.png'), Response::HTTP_GONE), + ])->preventStrayRequests(); + + $this->post('/test/image/url', [ + 'url' => 'https://example.com/client/image.bin', + ])->assertOk()->assertJson(['success' => 0]); + + $this->post('/test/image/url', [ + 'url' => 'https://example.com/server/image.bin', + ])->assertOk()->assertJson(['success' => 0]); + + Http::assertSentCount(2); + } + + /** + * Provides a list of valid image files to test. + * @return string[][] + */ + public function provideValidFiles(): array + { + return [ + 'gif' => [test_resource('responses/image.gif')], + 'jpg' => [test_resource('responses/image.jpg')], + 'png' => [test_resource('responses/image.png')], + 'svg' => [test_resource('responses/image.svg')], + 'svg' => [test_resource('responses/image.svg')], + ]; + } + + /** + * Provides a subset of the available image formats, since svg isn't supported by the GD library. + * @return string[][] + */ + public function provideValidFilesForImageUpload(): array + { + return Arr::except($this->provideValidFiles(), [ + 'svg', + ]); + } +} diff --git a/tests/Unit/Http/Controllers/EditorJsLinkControllerTest.php b/tests/Unit/Http/Controllers/EditorJsLinkControllerTest.php new file mode 100644 index 0000000..9db4f29 --- /dev/null +++ b/tests/Unit/Http/Controllers/EditorJsLinkControllerTest.php @@ -0,0 +1,62 @@ +afterApplicationCreated(function () { + Route::post('/test/url', [EditorJsLinkController::class, 'fetch']); + }); + } + + /** + * Checks simple URL fetch. + */ + public function testFetchValidUrl(): void { + Http::fake([ + 'https://example.com' => Http::response(file_get_contents(test_resource('responses/simple.html'))), + ])->preventStrayRequests(); + + $this->post('/test/url', [ + 'url' => 'https://example.com', + ])->assertOk()->assertJson([ + 'success' => 1, + 'meta' => [ + 'title' => 'Example Domain', + 'description' => 'This is a description', + ], + ]); + } + + /** + * Checks simple URL fetch. + */ + public function testImageDetermination(): void { + Http::fake([ + 'https://example.com' => Http::response(file_get_contents(test_resource('responses/with-image.html'))), + ])->preventStrayRequests(); + + $this->post('/test/url', [ + 'url' => 'https://example.com', + ])->assertOk()->assertJson([ + 'success' => 1, + 'meta' => [ + 'title' => 'Example Domain with an image', + 'description' => 'This is a description', + 'imageUrl' => 'https://example.com/image.jpg', + ], + ]); + } +} diff --git a/tests/helpers.php b/tests/helpers.php new file mode 100644 index 0000000..8d281e2 --- /dev/null +++ b/tests/helpers.php @@ -0,0 +1,25 @@ +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 W-aU^p<^Gq~1uYrAznw+{?*9*0hccu9 literal 0 HcmV?d00001 diff --git a/tests/resources/responses/image.jpg b/tests/resources/responses/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..442d5186b0f0a763a4a08fe3652897b6ed48d9c3 GIT binary patch literal 4566 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 literal 0 HcmV?d00001 diff --git a/tests/resources/responses/image.svg b/tests/resources/responses/image.svg new file mode 100644 index 0000000..2115515 --- /dev/null +++ b/tests/resources/responses/image.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/tests/resources/responses/image.txt b/tests/resources/responses/image.txt new file mode 100644 index 0000000..103f64e --- /dev/null +++ b/tests/resources/responses/image.txt @@ -0,0 +1 @@ +I am an image! diff --git a/tests/resources/responses/image.webp b/tests/resources/responses/image.webp new file mode 100644 index 0000000000000000000000000000000000000000..26702f56a03f08f5403085941683810760485473 GIT binary patch literal 4238 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 literal 0 HcmV?d00001 diff --git a/tests/resources/responses/simple.html b/tests/resources/responses/simple.html new file mode 100644 index 0000000..cc2d574 --- /dev/null +++ b/tests/resources/responses/simple.html @@ -0,0 +1,13 @@ + + + + + + + Example Domain + + + +

Hello World

+ + diff --git a/tests/resources/responses/with-image.html b/tests/resources/responses/with-image.html new file mode 100644 index 0000000..df57889 --- /dev/null +++ b/tests/resources/responses/with-image.html @@ -0,0 +1,19 @@ + + + + + + + Example Domain with an image + + + + + + + + + +

Hello World

+ + From 9cb4c75e1a1743a6065fdc39a369ea515ef892ed Mon Sep 17 00:00:00 2001 From: Roelof Roos Date: Sun, 17 Jul 2022 02:17:34 +0200 Subject: [PATCH 2/4] Added Guzzle as dependency It's likely already a dependency of most Laravel application --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index ce60969..cca8512 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "ext-exif": "*", "ext-json": "*", "codex-team/editor.js": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", "illuminate/support": "^8.0 || ^9.0", "laravel/nova": "^4.0", "spatie/image": "^1.7 || ^2.0" From be8a1f6189e7503d9ea8d9fb992b92729249d662 Mon Sep 17 00:00:00 2001 From: Roelof Roos Date: Sun, 17 Jul 2022 02:19:23 +0200 Subject: [PATCH 3/4] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cdb212..88aaa67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] +### Added +- Guzzle is now a dependency of this project. + +### Changed +- Improved image upload handling, using Laravel-native libraries +- Improved link metadata retrieval, using Laravel-native libraries + ## [3.0.3] ### Fixed From 1418caa04fa65331491198ca20412e8ed4db5732 Mon Sep 17 00:00:00 2001 From: Roelof Roos Date: Sun, 17 Jul 2022 02:20:59 +0200 Subject: [PATCH 4/4] Use Request instead of NovaRequest for EditorJsLinkController::fetch --- src/Http/Controllers/EditorJsLinkController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Controllers/EditorJsLinkController.php b/src/Http/Controllers/EditorJsLinkController.php index 7938d37..3fb5dfe 100644 --- a/src/Http/Controllers/EditorJsLinkController.php +++ b/src/Http/Controllers/EditorJsLinkController.php @@ -8,17 +8,17 @@ use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\RequestException; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Validator; -use Laravel\Nova\Http\Requests\NovaRequest; class EditorJsLinkController extends Controller { /** * Determine microdata for the given file. */ - public function fetch(NovaRequest $request): JsonResponse + public function fetch(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'url' => 'required|url',