From 4e78b5ea9e20ccd7d2d30f5896c7798c41f7f648 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 19 Apr 2024 11:11:25 -0700 Subject: [PATCH 1/6] [TM-803] Allow .jpeg images --- app/Http/Controllers/V2/Files/UploadController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/V2/Files/UploadController.php b/app/Http/Controllers/V2/Files/UploadController.php index d764e46d3..192ab9068 100644 --- a/app/Http/Controllers/V2/Files/UploadController.php +++ b/app/Http/Controllers/V2/Files/UploadController.php @@ -47,7 +47,9 @@ public function bulkUrlUpload(BulkUploadRequest $request, string $collection, Me // The downloadable file gets shuttled through the internals of Spatie without a chance for us to run // our own validations on them. png/jpg are the only mimes allowed for the photos collection according // to config/file-handling.php, and we disallow other collections than 'photos' above. - $handler = $mediaModel->addMediaFromUrl($data['download_url'], 'image/png', 'image/jpg'); + $handler = $mediaModel->addMediaFromUrl( + $data['download_url'], + 'image/png', 'image/jpg', 'image/jpeg'); $this->prepHandler($handler, $data, $mediaModel, $config, $collection); $details = $this->executeHandler($handler, $collection); From 22e866b9b044e55d3c17b44106f14daa81eb814c Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 19 Apr 2024 12:14:09 -0700 Subject: [PATCH 2/6] [TM-804] Store the created_by user id when uploading media. --- .../Controllers/V2/Files/UploadController.php | 2 ++ ...9_190707_add_created_by_to_media_table.php | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 database/migrations/2024_04_19_190707_add_created_by_to_media_table.php diff --git a/app/Http/Controllers/V2/Files/UploadController.php b/app/Http/Controllers/V2/Files/UploadController.php index 192ab9068..affef8680 100644 --- a/app/Http/Controllers/V2/Files/UploadController.php +++ b/app/Http/Controllers/V2/Files/UploadController.php @@ -9,6 +9,7 @@ use App\Models\V2\MediaModel; use Exception; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -129,6 +130,7 @@ private function saveAdditionalFileProperties($media, $data, $config) { $media->file_type = $this->getType($media, $config); $media->is_public = $data['is_public'] ?? true; + $media->created_by = Auth::user()->id; $media->save(); } diff --git a/database/migrations/2024_04_19_190707_add_created_by_to_media_table.php b/database/migrations/2024_04_19_190707_add_created_by_to_media_table.php new file mode 100644 index 000000000..dc6d595e0 --- /dev/null +++ b/database/migrations/2024_04_19_190707_add_created_by_to_media_table.php @@ -0,0 +1,29 @@ +foreignIdFor(User::class, 'created_by')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('media', function (Blueprint $table) { + $table->dropColumn(['created_by']); + }); + } +}; From 10a5d50ba4b748776fd78f11a2c95862f003ca7a Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 19 Apr 2024 13:43:11 -0700 Subject: [PATCH 3/6] [TM-804] Implement bulk delete --- app/Http/Controllers/V2/MediaController.php | 26 ++++++++ routes/api_v2.php | 1 + tests/V2/MediaControllerTest.php | 70 +++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 tests/V2/MediaControllerTest.php diff --git a/app/Http/Controllers/V2/MediaController.php b/app/Http/Controllers/V2/MediaController.php index ec96ad489..763e28cd3 100644 --- a/app/Http/Controllers/V2/MediaController.php +++ b/app/Http/Controllers/V2/MediaController.php @@ -3,10 +3,13 @@ namespace App\Http\Controllers\V2; use App\Http\Controllers\Controller; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Spatie\MediaLibrary\MediaCollections\Models\Media; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class MediaController extends Controller { @@ -32,4 +35,27 @@ public function delete(Request $request, string $uuid, string $collection = ''): return response()->json(['success' => 'media has been deleted'], 202); } + + public function bulkDelete(Request $request): JsonResponse + { + if (!Auth::user()->can('media-manage')) { + throw new AuthorizationException('No permission to bulk delete'); + } + + $uuids = $request->input('uuids'); + if (empty($uuids)) { + throw new NotFoundHttpException(); + } + + $media = Media::whereIn('uuid', $uuids)->where('created_by', Auth::user()->id); + if ($media->count() != count($uuids)) { + // If the bulk delete endpoint is being called for some media that weren't created by this user, + // avoid deleting any of them. + throw new AuthorizationException('Some of the media you are trying to delete were not created by you.'); + } + + $media->delete(); + + return response()->json(['success' => 'media has been deleted'], 202); + } } diff --git a/routes/api_v2.php b/routes/api_v2.php index 452e0d758..426143a3e 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -232,6 +232,7 @@ }); Route::prefix('media')->group(function () { + Route::delete('', [MediaController::class, 'bulkDelete']); Route::delete('/{uuid}', [MediaController::class, 'delete']); Route::delete('/{uuid}/{collection}', [MediaController::class, 'delete']); }); diff --git a/tests/V2/MediaControllerTest.php b/tests/V2/MediaControllerTest.php new file mode 100644 index 000000000..3126c40dc --- /dev/null +++ b/tests/V2/MediaControllerTest.php @@ -0,0 +1,70 @@ +serviceAccount()->create(); + $admin = User::factory()->admin()->create(); + Artisan::call('v2migration:roles'); + + $site = Site::factory()->ppc()->create(); + $photo1 = $site->addMedia(UploadedFile::fake()->image('photo1'))->toMediaCollection('photos'); + $photo1->update(['created_by' => $service->id]); + $photo2 = $site->addMedia(UploadedFile::fake()->image('photo2'))->toMediaCollection('photos'); + $photo2->update(['created_by' => $admin->id]); + $photo3 = $site->addMedia(UploadedFile::fake()->image('photo3'))->toMediaCollection('photos'); + $photo3->update(['created_by' => $service->id]); + + // No UUIDS is a 404 + $this->actingAs($service) + ->delete("/api/v2/media") + ->assertNotFound(); + + // Can't delete photo created by admin + $this->actingAs($service) + ->delete($this->buildBulkDeleteUrl([$photo1->uuid, $photo2->uuid])) + ->assertForbidden(); + $this->assertEquals(3, $site->refresh()->getMedia('photos')->count()); + + // Only service accounts can use bulk delete + $this->actingAs($admin) + ->delete($this->buildBulkDeleteUrl([$photo2->uuid])) + ->assertForbidden(); + $this->assertEquals(3, $site->refresh()->getMedia('photos')->count()); + + // Success case + $this->actingAs($service) + ->delete($this->buildBulkDeleteUrl([$photo1->uuid, $photo3->uuid])) + ->assertSuccessful(); + $this->assertEquals(1, $site->refresh()->getMedia('photos')->count()); + } + + private function buildBulkDeleteUrl($uuids): string + { + return '/api/v2/media?uuids[]=' . implode('&uuids[]=', $uuids); + } +} From 1f614c510fc0a6683951b28884cc9a0bfddf81b5 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 19 Apr 2024 13:55:28 -0700 Subject: [PATCH 4/6] [TM-804] Document the bulk delete endpoint. --- openapi-src/V2/paths/_index.yml | 16 ++++++++++++++++ resources/docs/swagger-v2.yml | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/openapi-src/V2/paths/_index.yml b/openapi-src/V2/paths/_index.yml index f7b12bcbe..ec43250d3 100644 --- a/openapi-src/V2/paths/_index.yml +++ b/openapi-src/V2/paths/_index.yml @@ -931,6 +931,22 @@ type: array items: $ref: '../definitions/_index.yml#/V2FileRead' +/v2/media: + delete: + summary: Bulk delete a set of media by UUID + operationId: v2-bulk-delete-media + tags: + - Media + parameters: + - type: array + name: uuids[] + in: query + required: true + items: + type: string + responses: + '200': + description: OK /v2/files/{UUID}: put: summary: Update properties of a specific file diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index 18ba8da3e..74a86ac98 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -69769,6 +69769,22 @@ paths: type: boolean created_at: type: string + /v2/media: + delete: + summary: Bulk delete a set of media by UUID + operationId: v2-bulk-delete-media + tags: + - Media + parameters: + - type: array + name: 'uuids[]' + in: query + required: true + items: + type: string + responses: + '200': + description: OK '/v2/files/{UUID}': put: summary: Update properties of a specific file From 636519b4f8712b26838cf18e2a4a408c6bfbd61e Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 19 Apr 2024 14:07:18 -0700 Subject: [PATCH 5/6] [TM-804] Lint fix. --- app/Http/Controllers/V2/Files/UploadController.php | 5 ++++- app/Http/Controllers/V2/MediaController.php | 2 +- ...24_04_19_190707_add_created_by_to_media_table.php | 3 +-- tests/V2/MediaControllerTest.php | 12 +----------- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/V2/Files/UploadController.php b/app/Http/Controllers/V2/Files/UploadController.php index affef8680..7a00279cb 100644 --- a/app/Http/Controllers/V2/Files/UploadController.php +++ b/app/Http/Controllers/V2/Files/UploadController.php @@ -50,7 +50,10 @@ public function bulkUrlUpload(BulkUploadRequest $request, string $collection, Me // to config/file-handling.php, and we disallow other collections than 'photos' above. $handler = $mediaModel->addMediaFromUrl( $data['download_url'], - 'image/png', 'image/jpg', 'image/jpeg'); + 'image/png', + 'image/jpg', + 'image/jpeg' + ); $this->prepHandler($handler, $data, $mediaModel, $config, $collection); $details = $this->executeHandler($handler, $collection); diff --git a/app/Http/Controllers/V2/MediaController.php b/app/Http/Controllers/V2/MediaController.php index 763e28cd3..4631e0024 100644 --- a/app/Http/Controllers/V2/MediaController.php +++ b/app/Http/Controllers/V2/MediaController.php @@ -38,7 +38,7 @@ public function delete(Request $request, string $uuid, string $collection = ''): public function bulkDelete(Request $request): JsonResponse { - if (!Auth::user()->can('media-manage')) { + if (! Auth::user()->can('media-manage')) { throw new AuthorizationException('No permission to bulk delete'); } diff --git a/database/migrations/2024_04_19_190707_add_created_by_to_media_table.php b/database/migrations/2024_04_19_190707_add_created_by_to_media_table.php index dc6d595e0..77cbd2698 100644 --- a/database/migrations/2024_04_19_190707_add_created_by_to_media_table.php +++ b/database/migrations/2024_04_19_190707_add_created_by_to_media_table.php @@ -5,8 +5,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/tests/V2/MediaControllerTest.php b/tests/V2/MediaControllerTest.php index 3126c40dc..e558db516 100644 --- a/tests/V2/MediaControllerTest.php +++ b/tests/V2/MediaControllerTest.php @@ -3,21 +3,11 @@ namespace Tests\V2; use App\Models\User; -use App\Models\V2\Nurseries\Nursery; -use App\Models\V2\Nurseries\NurseryReport; -use App\Models\V2\Organisation; -use App\Models\V2\Projects\Project; -use App\Models\V2\Projects\ProjectMonitoring; -use App\Models\V2\Projects\ProjectReport; use App\Models\V2\Sites\Site; -use App\Models\V2\Sites\SiteMonitoring; -use App\Models\V2\Sites\SiteReport; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Artisan; -use Illuminate\Support\Facades\Storage; -use Spatie\MediaLibrary\MediaCollections\Models\Media; use Tests\TestCase; final class MediaControllerTest extends TestCase @@ -41,7 +31,7 @@ public function test_bulk_delete(): void // No UUIDS is a 404 $this->actingAs($service) - ->delete("/api/v2/media") + ->delete('/api/v2/media') ->assertNotFound(); // Can't delete photo created by admin From 8d8873fcf3e7b7b459b25c3db09e2b7408b58a40 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Fri, 19 Apr 2024 15:34:16 -0700 Subject: [PATCH 6/6] [TM-803] We can use images hosted in the local docker container for this test. --- tests/V2/Files/UploadControllerTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/V2/Files/UploadControllerTest.php b/tests/V2/Files/UploadControllerTest.php index e11fe2c8c..c98fffb77 100644 --- a/tests/V2/Files/UploadControllerTest.php +++ b/tests/V2/Files/UploadControllerTest.php @@ -546,10 +546,8 @@ public function test_bulk_upload_functionality() $service = User::factory()->serviceAccount()->create(); Artisan::call('v2migration:roles'); $site = Site::factory()->create(); - // It's not ideal for the testing suite to use a real hosted image, but I haven't found a way to fake a - // http download URL in phpunit/spatie. - $url = 'https://new-wri-prod.wri-restoration-marketplace-api.com/images/V2/land-tenures/national-protected-area.png'; - $badMimeUrl = 'https://www.terramatch.org/images/landing-page-hero-banner.webp'; + $url = 'http://localhost/images/V2/land-tenures/national-protected-area.png'; + $badMimeUrl = 'http://localhost/images/email_logo.gif'; // Check a valid upload $this->actingAs($service)