From b0d835d1425942bfd77c1d495ea927ea86339841 Mon Sep 17 00:00:00 2001 From: soeren Date: Sun, 7 Dec 2025 17:39:23 +0100 Subject: [PATCH] sparkbooth anbindung gefixt --- app/Filament/Pages/SparkboothConnections.php | 18 +- app/Filament/Pages/SparkboothSetup.php | 34 ++ .../Api/SparkboothUploadController.php | 312 ++++++++++++++++-- app/Models/Gallery.php | 14 + database/factories/GalleryFactory.php | 3 + ...rkbooth_credentials_to_galleries_table.php | 38 +++ database/seeders/GallerySeeder.php | 3 + .../pages/partials/sparkbooth-token.blade.php | 35 +- .../filament/pages/sparkbooth-setup.blade.php | 63 +++- tests/Feature/SparkboothUploadTest.php | 117 +++++++ 10 files changed, 597 insertions(+), 40 deletions(-) create mode 100644 database/migrations/2025_12_07_160306_add_sparkbooth_credentials_to_galleries_table.php create mode 100644 tests/Feature/SparkboothUploadTest.php diff --git a/app/Filament/Pages/SparkboothConnections.php b/app/Filament/Pages/SparkboothConnections.php index 84d819c..058b082 100644 --- a/app/Filament/Pages/SparkboothConnections.php +++ b/app/Filament/Pages/SparkboothConnections.php @@ -50,6 +50,10 @@ class SparkboothConnections extends Page implements HasTable ->label('Upload-Pfad') ->copyable() ->toggleable(), + TextColumn::make('sparkbooth_username') + ->label('Benutzername') + ->copyable() + ->toggleable(), TextColumn::make('created_at') ->label('Angelegt') ->since() @@ -62,7 +66,7 @@ class SparkboothConnections extends Page implements HasTable ->color('primary') ->modalHeading('Upload-Zugangsdaten') ->modalSubmitAction(false) - ->modalCancelActionLabel('Schließen') + ->modalCancelActionLabel('Schliessen') ->modalContent(function (Gallery $record) { $plainToken = $record->regenerateUploadToken(); @@ -71,11 +75,14 @@ class SparkboothConnections extends Page implements HasTable 'upload_token' => $plainToken, 'upload_url' => route('api.sparkbooth.upload'), 'gallery_url' => route('gallery.show', $record), + 'sparkbooth_username' => $record->sparkbooth_username, + 'sparkbooth_password' => $record->sparkbooth_password, + 'response_format' => $record->sparkbooth_response_format, ]; Notification::make() - ->title('Upload-Token wurde erneuert.') - ->body('Bitte verwende den neuen Token in Sparkbooth.') + ->title('Zugangsdaten aktualisiert.') + ->body('Der Upload-Token wurde erneuert. Username/Passwort bleiben unveraendert.') ->success() ->send(); @@ -83,6 +90,9 @@ class SparkboothConnections extends Page implements HasTable }), ]) ->emptyStateHeading('Keine Sparkbooth-Verbindungen') - ->emptyStateDescription('Lege eine neue Verbindung an oder aktiviere Uploads für eine Galerie.'); + ->emptyStateDescription('Lege eine neue Verbindung an oder aktiviere Uploads fuer eine Galerie.'); } } + + + diff --git a/app/Filament/Pages/SparkboothSetup.php b/app/Filament/Pages/SparkboothSetup.php index c52713d..8efb1e6 100644 --- a/app/Filament/Pages/SparkboothSetup.php +++ b/app/Filament/Pages/SparkboothSetup.php @@ -4,6 +4,7 @@ namespace App\Filament\Pages; use App\Models\Gallery; use BackedEnum; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Concerns\InteractsWithForms; @@ -62,6 +63,23 @@ class SparkboothSetup extends Page implements HasForms Toggle::make('upload_enabled') ->label('Uploads aktivieren') ->default(true), + TextInput::make('sparkbooth_username') + ->label('Sparkbooth Benutzername') + ->helperText('Wird in Sparkbooth unter „Username“ eingetragen. Erlaubt sind Buchstaben, Zahlen sowie ._-') + ->default(fn (): string => 'spark-'.Str::lower(Str::random(6))) + ->required() + ->maxLength(64) + ->rule('regex:/^[A-Za-z0-9._-]+$/') + ->unique(table: Gallery::class, column: 'sparkbooth_username'), + Select::make('sparkbooth_response_format') + ->label('Standard-Antwortformat') + ->helperText('Sparkbooth kann JSON oder XML erwarten. JSON ist empfohlen.') + ->options([ + 'json' => 'JSON', + 'xml' => 'XML', + ]) + ->default('json') + ->required(), ]) ->statePath('data'); } @@ -70,6 +88,9 @@ class SparkboothSetup extends Page implements HasForms { $data = $this->form->getState(); + $sparkboothUsername = $this->normalizeUsername($data['sparkbooth_username'] ?? ''); + $sparkboothPassword = Str::random(24); + $gallery = new Gallery([ 'name' => $data['name'], 'title' => $data['title'], @@ -78,6 +99,9 @@ class SparkboothSetup extends Page implements HasForms 'allow_ai_styles' => (bool) $data['allow_ai_styles'], 'allow_print' => (bool) $data['allow_print'], 'upload_enabled' => (bool) $data['upload_enabled'], + 'sparkbooth_username' => $sparkboothUsername, + 'sparkbooth_password' => $sparkboothPassword, + 'sparkbooth_response_format' => $data['sparkbooth_response_format'] ?? 'json', ]); $gallery->slug = Str::uuid()->toString(); @@ -91,6 +115,9 @@ class SparkboothSetup extends Page implements HasForms 'upload_token' => $plainToken, 'upload_url' => route('api.sparkbooth.upload'), 'gallery_url' => route('gallery.show', $gallery), + 'sparkbooth_username' => $sparkboothUsername, + 'sparkbooth_password' => $sparkboothPassword, + 'response_format' => $gallery->sparkbooth_response_format, ]; Notification::make() @@ -114,4 +141,11 @@ class SparkboothSetup extends Page implements HasForms ->url(route('filament.admin.pages.dashboard')), ]; } + + protected function normalizeUsername(string $value): string + { + $clean = preg_replace('/[^A-Za-z0-9._-]/', '', $value) ?? ''; + + return Str::of($clean)->lower()->trim()->value(); + } } diff --git a/app/Http/Controllers/Api/SparkboothUploadController.php b/app/Http/Controllers/Api/SparkboothUploadController.php index cb43d2c..92d32c5 100644 --- a/app/Http/Controllers/Api/SparkboothUploadController.php +++ b/app/Http/Controllers/Api/SparkboothUploadController.php @@ -6,59 +6,110 @@ use App\Http\Controllers\Controller; use App\Models\Gallery; use App\Models\Image; use Illuminate\Http\Request; -use Illuminate\Support\Facades\File; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; +use RuntimeException; use Symfony\Component\HttpFoundation\Response; class SparkboothUploadController extends Controller { + private const ALLOWED_MIMES = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/bmp', + 'image/webp', + ]; + + private const MAX_FILE_SIZE = 10240 * 1024; // 10 MB + public function store(Request $request) { - $request->validate([ - 'token' => ['required', 'string'], - 'file' => ['required', 'file', 'mimes:jpeg,png,gif,bmp,webp', 'max:10240'], - 'filename' => ['nullable', 'string'], - ]); + $this->validateRequest($request); - $gallery = $this->resolveGalleryByToken($request->string('token')); + [$gallery, $authMethod] = $this->resolveGallery($request); if (! $gallery) { - return response()->json(['error' => 'Invalid token.'], Response::HTTP_FORBIDDEN); + return $this->respondError($request, 'Invalid credentials.', Response::HTTP_FORBIDDEN); } if (! $gallery->upload_enabled) { - return response()->json(['error' => 'Uploads are disabled for this gallery.'], Response::HTTP_FORBIDDEN); + return $this->respondError($request, 'Uploads are disabled for this gallery.', Response::HTTP_FORBIDDEN, $gallery); } - if ($gallery->upload_token_expires_at && now()->greaterThanOrEqualTo($gallery->upload_token_expires_at)) { - return response()->json(['error' => 'Upload token expired.'], Response::HTTP_FORBIDDEN); + if ($authMethod === 'token' + && $gallery->upload_token_expires_at + && now()->greaterThanOrEqualTo($gallery->upload_token_expires_at) + ) { + return $this->respondError($request, 'Upload token expired.', Response::HTTP_FORBIDDEN, $gallery); } - $file = $request->file('file'); - $safeName = $this->buildFilename($file->getClientOriginalExtension(), $request->input('filename')); - $relativePath = trim($gallery->images_path, '/').'/'.$safeName; - $destinationPath = public_path('storage/'.dirname($relativePath)); + [$file, $base64Payload] = $this->extractMediaPayload($request); - if (! File::exists($destinationPath)) { - File::makeDirectory($destinationPath, 0755, true); + if (! $file && $base64Payload === null) { + return $this->respondError($request, 'No media payload provided.', Response::HTTP_BAD_REQUEST, $gallery); } - $file->move($destinationPath, basename($relativePath)); + try { + [$relativePath, $publicUrl] = $this->persistMedia( + $gallery, + $file, + $base64Payload, + $request->input('filename') + ); + } catch (RuntimeException $exception) { + return $this->respondError($request, $exception->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY, $gallery); + } - $image = Image::create([ + Image::create([ 'gallery_id' => $gallery->id, 'path' => $relativePath, 'is_public' => true, ]); - return response()->json([ - 'message' => 'Upload ok', - 'image_id' => $image->id, - 'url' => asset('storage/'.$relativePath), + return $this->respondSuccess($request, $publicUrl, $gallery); + } + + private function validateRequest(Request $request): void + { + $request->validate([ + 'token' => ['nullable', 'string'], + 'username' => ['nullable', 'string'], + 'password' => ['nullable', 'string'], + 'media' => ['nullable'], + 'file' => ['nullable', 'file', 'mimes:jpeg,png,gif,bmp,webp', 'max:10240'], + 'filename' => ['nullable', 'string'], + 'name' => ['nullable', 'string'], + 'email' => ['nullable', 'string'], + 'message' => ['nullable', 'string'], + 'response_format' => ['nullable', 'in:json,xml'], ]); } + /** + * @return array{0: ?Gallery, 1: ?string} + */ + private function resolveGallery(Request $request): array + { + if ($request->filled('token')) { + return [$this->resolveGalleryByToken($request->string('token')), 'token']; + } + + if ($request->filled('username')) { + return [ + $this->resolveGalleryByCredentials( + $request->string('username'), + $request->string('password') + ), + 'credentials', + ]; + } + + return [null, null]; + } + private function resolveGalleryByToken(string $token): ?Gallery { $galleries = Gallery::query() @@ -75,13 +126,226 @@ class SparkboothUploadController extends Controller return null; } - private function buildFilename(string $extension, ?string $preferred = null): string + private function resolveGalleryByCredentials(?string $username, ?string $password): ?Gallery + { + if (blank($username) || blank($password)) { + return null; + } + + $normalized = Str::of($username)->lower()->trim()->value(); + + $gallery = Gallery::query() + ->whereNotNull('sparkbooth_username') + ->where('sparkbooth_username', $normalized) + ->first(); + + if (! $gallery) { + return null; + } + + if (! hash_equals((string) $gallery->sparkbooth_password, (string) $password)) { + return null; + } + + return $gallery; + } + + /** + * @return array{0: ?UploadedFile, 1: ?string} + */ + private function extractMediaPayload(Request $request): array + { + $file = $request->file('media') ?? $request->file('file'); + + if ($file) { + return [$file, null]; + } + + $payload = $request->input('media'); + + if ($payload === null || $payload === '') { + return [null, null]; + } + + return [null, $payload]; + } + + /** + * @return array{0: string, 1: string} + */ + private function persistMedia( + Gallery $gallery, + ?UploadedFile $file, + ?string $base64Payload, + ?string $preferredFilename + ): array { + $directory = trim($gallery->images_path, '/'); + + if ($directory === '') { + $directory = 'uploads'; + } + + if ($file) { + $extension = $file->getClientOriginalExtension(); + $filename = $this->buildFilename($extension, $preferredFilename); + $relativePath = $directory.'/'.$filename; + + $file->storeAs($directory, $filename, 'public'); + + return [$relativePath, asset('storage/'.$relativePath)]; + } + + if ($base64Payload === null) { + throw new RuntimeException('No media payload provided.'); + } + + [$binary, $extension] = $this->decodeBase64Media($base64Payload); + $filename = $this->buildFilename($extension, $preferredFilename); + $relativePath = $directory.'/'.$filename; + + if (! Storage::disk('public')->put($relativePath, $binary)) { + throw new RuntimeException('Failed to store uploaded image.'); + } + + return [$relativePath, asset('storage/'.$relativePath)]; + } + + /** + * @return array{0: string, 1: string} + */ + private function decodeBase64Media(string $payload): array + { + $mime = null; + + if (preg_match('/^data:(image\\/[^;]+);base64,(.+)$/', $payload, $matches)) { + $mime = $matches[1]; + $payload = $matches[2]; + } + + $binary = base64_decode($payload, true); + + if ($binary === false) { + throw new RuntimeException('Invalid media payload.'); + } + + if (strlen($binary) > self::MAX_FILE_SIZE) { + throw new RuntimeException('File too large.'); + } + + if (! $mime) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = $finfo ? finfo_buffer($finfo, $binary) : null; + + if ($finfo) { + finfo_close($finfo); + } + } + + if (! $mime || ! $this->isAllowedMime($mime)) { + throw new RuntimeException('Unsupported image type.'); + } + + $extension = $this->extensionFromMime($mime) ?? 'jpg'; + + return [$binary, $extension]; + } + + private function isAllowedMime(string $mime): bool + { + return in_array(strtolower($mime), self::ALLOWED_MIMES, true); + } + + private function extensionFromMime(string $mime): ?string + { + return match (strtolower($mime)) { + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/bmp' => 'bmp', + 'image/webp' => 'webp', + default => null, + }; + } + + private function buildFilename(?string $extension, ?string $preferred = null): string { $extension = strtolower($extension ?: 'jpg'); $base = $preferred ? Str::slug(pathinfo($preferred, PATHINFO_FILENAME)) : 'sparkbooth_'.now()->format('Ymd_His').'_'.Str::random(6); + if ($base === '') { + $base = 'sparkbooth_'.now()->format('Ymd_His').'_'.Str::random(6); + } + return $base.'.'.$extension; } + + private function respondSuccess(Request $request, string $url, ?Gallery $gallery = null) + { + $format = $this->determineResponseFormat($request, $gallery); + + if ($format === 'xml') { + $body = $this->buildXmlResponse('ok', $url); + + return response($body, Response::HTTP_OK)->header('Content-Type', 'application/xml'); + } + + return response()->json([ + 'status' => true, + 'error' => null, + 'url' => $url, + ]); + } + + private function respondError(Request $request, string $message, int $status, ?Gallery $gallery = null) + { + $format = $this->determineResponseFormat($request, $gallery); + + if ($format === 'xml') { + $body = $this->buildXmlResponse('fail', null, $message); + + return response($body, $status)->header('Content-Type', 'application/xml'); + } + + return response()->json([ + 'status' => false, + 'error' => $message, + 'url' => null, + ], $status); + } + + private function determineResponseFormat(Request $request, ?Gallery $gallery = null): string + { + $candidate = Str::of((string) $request->input('response_format'))->lower()->value(); + + if (in_array($candidate, ['json', 'xml'], true)) { + return $candidate; + } + + $accept = Str::of((string) $request->header('Accept'))->lower()->value(); + + if (str_contains($accept, 'application/xml') || str_contains($accept, 'text/xml')) { + return 'xml'; + } + + if ($gallery && in_array($gallery->sparkbooth_response_format, ['json', 'xml'], true)) { + return $gallery->sparkbooth_response_format; + } + + return 'json'; + } + + private function buildXmlResponse(string $status, ?string $url = null, ?string $errorMessage = null): string + { + if ($status === 'ok') { + $urlAttr = $url ? ' url="'.e($url).'"' : ''; + + return ''; + } + + $errorAttr = $errorMessage ? '' : ''; + + return ''.$errorAttr.''; + } } diff --git a/app/Models/Gallery.php b/app/Models/Gallery.php index a8967e4..d15a793 100644 --- a/app/Models/Gallery.php +++ b/app/Models/Gallery.php @@ -26,6 +26,9 @@ class Gallery extends Model 'upload_enabled', 'upload_token_hash', 'upload_token_expires_at', + 'sparkbooth_username', + 'sparkbooth_password', + 'sparkbooth_response_format', ]; protected function casts(): array @@ -39,6 +42,8 @@ class Gallery extends Model 'access_duration_minutes' => 'int', 'upload_enabled' => 'bool', 'upload_token_expires_at' => 'datetime', + 'sparkbooth_password' => 'encrypted', + 'sparkbooth_response_format' => 'string', ]; } @@ -60,4 +65,13 @@ class Gallery extends Model return $token; } + + public function regenerateSparkboothPassword(): string + { + $password = \Illuminate\Support\Str::random(24); + $this->sparkbooth_password = $password; + $this->save(); + + return $password; + } } diff --git a/database/factories/GalleryFactory.php b/database/factories/GalleryFactory.php index e0bbaba..912e66d 100644 --- a/database/factories/GalleryFactory.php +++ b/database/factories/GalleryFactory.php @@ -35,6 +35,9 @@ class GalleryFactory extends Factory 'upload_enabled' => false, 'upload_token_hash' => null, 'upload_token_expires_at' => null, + 'sparkbooth_username' => 'spark-'.$this->faker->regexify('[a-z0-9]{6}'), + 'sparkbooth_password' => 'pw-'.$this->faker->regexify('[A-Za-z0-9]{10}'), + 'sparkbooth_response_format' => 'json', ]; } } diff --git a/database/migrations/2025_12_07_160306_add_sparkbooth_credentials_to_galleries_table.php b/database/migrations/2025_12_07_160306_add_sparkbooth_credentials_to_galleries_table.php new file mode 100644 index 0000000..f41f2a4 --- /dev/null +++ b/database/migrations/2025_12_07_160306_add_sparkbooth_credentials_to_galleries_table.php @@ -0,0 +1,38 @@ +string('sparkbooth_username')->nullable()->after('upload_token_expires_at'); + $table->text('sparkbooth_password')->nullable()->after('sparkbooth_username'); + $table->string('sparkbooth_response_format', 10)->default('json')->after('sparkbooth_password'); + + $table->unique('sparkbooth_username'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('galleries', function (Blueprint $table): void { + $table->dropUnique(['sparkbooth_username']); + + $table->dropColumn([ + 'sparkbooth_username', + 'sparkbooth_password', + 'sparkbooth_response_format', + ]); + }); + } +}; diff --git a/database/seeders/GallerySeeder.php b/database/seeders/GallerySeeder.php index 10ea0c1..cc5533c 100644 --- a/database/seeders/GallerySeeder.php +++ b/database/seeders/GallerySeeder.php @@ -33,6 +33,9 @@ class GallerySeeder extends Seeder 'upload_enabled' => false, 'upload_token_hash' => null, 'upload_token_expires_at' => null, + 'sparkbooth_username' => null, + 'sparkbooth_password' => null, + 'sparkbooth_response_format' => 'json', ] ); } diff --git a/resources/views/filament/pages/partials/sparkbooth-token.blade.php b/resources/views/filament/pages/partials/sparkbooth-token.blade.php index 3383a79..8601532 100644 --- a/resources/views/filament/pages/partials/sparkbooth-token.blade.php +++ b/resources/views/filament/pages/partials/sparkbooth-token.blade.php @@ -6,15 +6,44 @@
-

Upload Token

-

{{ $upload_token }}

-

Trage diesen Token in Sparkbooth unter „Upload Secret“ ein.

+

Sparkbooth Benutzername

+

{{ $sparkbooth_username ?? '—' }}

+

Eintragen im Sparkbooth Custom Upload Dialog unter „Username“.

+
+

Sparkbooth Passwort

+

{{ $sparkbooth_password ?? '—' }}

+

Eintragen unter „Password“.

+
+
+ +

Galerie-Link

{{ $gallery_url }}

Slug: {{ $gallery['slug'] }}, Pfad: storage/{{ $gallery['images_path'] }}

+
+

Antwortformat

+

{{ strtoupper($response_format ?? 'JSON') }}

+

Muss mit „JSON Response“ oder „XML Response“ in Sparkbooth übereinstimmen.

+
+
+ +
+
+

Legacy Upload Token

+

{{ $upload_token }}

+

Optionales Secret (Feld „token“) für ältere Integrationen.

+
+
+

Sparkbooth Hinweise

+
    +
  • Uploader „Custom Upload“ wählen.
  • +
  • Username & Password wie oben eintragen.
  • +
  • Falls JSON Response aktiviert ist, hier ebenfalls JSON wählen (gleiches gilt für XML).
  • +
+
diff --git a/resources/views/filament/pages/sparkbooth-setup.blade.php b/resources/views/filament/pages/sparkbooth-setup.blade.php index 8edf94c..da66da0 100644 --- a/resources/views/filament/pages/sparkbooth-setup.blade.php +++ b/resources/views/filament/pages/sparkbooth-setup.blade.php @@ -15,15 +15,44 @@
-

Upload Token

-

{{ $result['upload_token'] }}

-

Trage diesen Token in Sparkbooth unter „Upload Secret“ ein.

+

Sparkbooth Benutzername

+

{{ $result['sparkbooth_username'] }}

+

Eintragen in Sparkbooth ➜ Settings ➜ Upload ➜ Custom Upload ➜ Username.

+
+

Sparkbooth Passwort

+

{{ $result['sparkbooth_password'] }}

+

Eintragen in Sparkbooth unter „Password“.

+
+
+ +

Galerie-Link

{{ $result['gallery_url'] }}

Slug: {{ $result['gallery']['slug'] }}, Pfad: storage/{{ $result['gallery']['images_path'] }}

+
+

Standard-Antwortformat

+

{{ strtoupper($result['response_format']) }}

+

Muss mit der Auswahl „JSON Response“ oder „XML Response“ in Sparkbooth übereinstimmen.

+
+
+ +
+
+

Legacy Upload Token

+

{{ $result['upload_token'] }}

+

Optional: Für bestehende Integrationen nutzbar (Feld „token“).

+
+
+

Sparkbooth Hinweise

+
    +
  • Uploader: „Custom Upload“ wählen.
  • +
  • URL: {{ $result['upload_url'] }}
  • +
  • Username/Password eintragen, optional Message.
  • +
+
@@ -41,14 +70,30 @@
-
-

Sparkbooth Beispiel (Custom Upload)

+
+
+

Sparkbooth Beispiel (Custom Upload)

-POST {{ $result['upload_url'] }}
-Content-Type: multipart/form-data
-token={{ $result['upload_token'] }}
-file=@your-photo.jpg
+curl -X POST {{ $result['upload_url'] }} \
+  -F "media=@your-photo.jpg" \
+  -F "username={{ $result['sparkbooth_username'] }}" \
+  -F "password={{ $result['sparkbooth_password'] }}" \
+  -F "name=Guest" \
+  -F "message=Thanks!"
 
+
+
+

Erwartete Server Responses

+
+
{
+  "status": true,
+  "error": null,
+  "url": "{{ $result['gallery_url'] }}"
+}
+
<?xml version="1.0" encoding="UTF-8"?>
+<rsp status="ok" url="{{ $result['gallery_url'] }}" />
+
+
@endif diff --git a/tests/Feature/SparkboothUploadTest.php b/tests/Feature/SparkboothUploadTest.php new file mode 100644 index 0000000..2276fc0 --- /dev/null +++ b/tests/Feature/SparkboothUploadTest.php @@ -0,0 +1,117 @@ +create([ + 'upload_enabled' => true, + 'images_path' => 'uploads/test', + 'sparkbooth_username' => 'spark-user', + 'sparkbooth_password' => 'secret-123', + 'sparkbooth_response_format' => 'json', + ]); + + $file = UploadedFile::fake()->image('photo.jpg', 800, 600); + + $response = $this->postJson(route('api.sparkbooth.upload'), [ + 'username' => 'spark-user', + 'password' => 'secret-123', + 'media' => $file, + 'name' => 'Guest', + 'message' => 'Hallo', + ]); + + $response->assertOk() + ->assertJson([ + 'status' => true, + 'error' => null, + ]) + ->assertJsonStructure(['url']); + + $image = Image::first(); + $this->assertNotNull($image); + $this->assertSame($gallery->id, $image->gallery_id); + $this->assertStringStartsWith('uploads/test/sparkbooth_', $image->path); + + Storage::disk('public')->assertExists($image->path); + } + + public function test_xml_response_for_invalid_credentials(): void + { + Storage::fake('public'); + + Gallery::factory()->create([ + 'upload_enabled' => true, + 'images_path' => 'uploads/test', + 'sparkbooth_username' => 'spark-user', + 'sparkbooth_password' => 'secret-123', + 'sparkbooth_response_format' => 'xml', + ]); + + $file = UploadedFile::fake()->image('photo.jpg', 800, 600); + + $response = $this->post(route('api.sparkbooth.upload'), [ + 'username' => 'spark-user', + 'password' => 'wrong', + 'media' => $file, + 'response_format' => 'xml', + ]); + + $response->assertStatus(403); + $response->assertHeader('Content-Type', 'application/xml'); + $this->assertStringContainsString('', $response->getContent()); + } + + public function test_base64_upload_with_token_succeeds(): void + { + Storage::fake('public'); + + $gallery = Gallery::factory()->create([ + 'upload_enabled' => true, + 'images_path' => 'uploads/base64', + 'sparkbooth_response_format' => 'json', + ]); + + $token = 'tokentest123'; + $gallery->setUploadToken($token); + $gallery->save(); + + $fake = UploadedFile::fake()->image('inline.png', 20, 20); + $binary = base64_encode(file_get_contents($fake->getRealPath())); + $dataUri = 'data:image/png;base64,'.$binary; + + $response = $this->postJson(route('api.sparkbooth.upload'), [ + 'token' => $token, + 'media' => $dataUri, + 'filename' => 'custom.png', + ]); + + $response->assertOk() + ->assertJson([ + 'status' => true, + 'error' => null, + ]) + ->assertJsonStructure(['url']); + + Storage::disk('public')->assertExists('uploads/base64/custom.png'); + + $this->assertDatabaseHas('images', [ + 'gallery_id' => $gallery->id, + 'path' => 'uploads/base64/custom.png', + ]); + } +}