sparkbooth anbindung gefixt
This commit is contained in:
@@ -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 '<?xml version="1.0" encoding="UTF-8"?><rsp status="ok"'.$urlAttr.' />';
|
||||
}
|
||||
|
||||
$errorAttr = $errorMessage ? '<err msg="'.e($errorMessage).'" />' : '';
|
||||
|
||||
return '<?xml version="1.0" encoding="UTF-8"?><rsp status="fail">'.$errorAttr.'</rsp>';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user