154 lines
4.9 KiB
PHP
154 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Studio;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Services\News\NewsCoverImageService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use RuntimeException;
|
|
|
|
final class StudioNewsMediaApiController extends Controller
|
|
{
|
|
public function __construct(private readonly NewsCoverImageService $covers)
|
|
{
|
|
}
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$this->authorizeNews($request);
|
|
|
|
$validated = $request->validate([
|
|
'image' => [
|
|
'required',
|
|
'file',
|
|
'image',
|
|
'max:' . $this->covers->maxFileSizeKb(),
|
|
'mimes:jpg,jpeg,png,webp',
|
|
'mimetypes:image/jpeg,image/png,image/webp',
|
|
],
|
|
]);
|
|
|
|
$file = $validated['image'];
|
|
|
|
try {
|
|
$stored = $this->covers->storeUploadedFile($file);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'path' => $stored['path'],
|
|
'url' => $stored['url'],
|
|
'width' => $stored['width'],
|
|
'height' => $stored['height'],
|
|
'mime_type' => 'image/webp',
|
|
'size_bytes' => $stored['size_bytes'],
|
|
'mobile_url' => $stored['mobile_url'],
|
|
'desktop_url' => $stored['desktop_url'],
|
|
'srcset' => $stored['srcset'],
|
|
]);
|
|
} catch (RuntimeException $e) {
|
|
return response()->json([
|
|
'error' => 'Validation failed',
|
|
'message' => $e->getMessage(),
|
|
], 422);
|
|
} catch (\Throwable $e) {
|
|
logger()->error('News media upload failed', [
|
|
'user_id' => (int) ($request->user()?->id ?? 0),
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
|
|
return response()->json([
|
|
'error' => 'Upload failed',
|
|
'message' => 'Could not upload image right now.',
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function destroy(Request $request): JsonResponse
|
|
{
|
|
$this->authorizeNews($request);
|
|
|
|
$validated = $request->validate([
|
|
'path' => ['required', 'string', 'max:2048'],
|
|
]);
|
|
|
|
$this->covers->deleteManagedFiles((string) $validated['path']);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array{path:string,width:int,height:int,size_bytes:int}
|
|
*/
|
|
private function storeMediaFile(UploadedFile $file): array
|
|
{
|
|
$this->assertImageManager();
|
|
$this->assertStorageIsAllowed();
|
|
|
|
$uploadPath = (string) ($file->getRealPath() ?: $file->getPathname());
|
|
|
|
if ($uploadPath === '' || ! is_readable($uploadPath)) {
|
|
throw new RuntimeException('Unable to resolve uploaded image path.');
|
|
}
|
|
|
|
$raw = file_get_contents($uploadPath);
|
|
if ($raw === false || $raw === '') {
|
|
throw new RuntimeException('Unable to read uploaded image.');
|
|
}
|
|
|
|
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
|
$mime = strtolower((string) $finfo->buffer($raw));
|
|
if (! in_array($mime, self::ALLOWED_MIME_TYPES, true)) {
|
|
throw new RuntimeException('Unsupported image mime type.');
|
|
}
|
|
|
|
$size = @getimagesizefromstring($raw);
|
|
if (! is_array($size) || ($size[0] ?? 0) < 1 || ($size[1] ?? 0) < 1) {
|
|
throw new RuntimeException('Uploaded file is not a valid image.');
|
|
}
|
|
|
|
$width = (int) ($size[0] ?? 0);
|
|
$height = (int) ($size[1] ?? 0);
|
|
|
|
if ($width < self::MIN_WIDTH || $height < self::MIN_HEIGHT) {
|
|
throw new RuntimeException(sprintf(
|
|
'Image is too small. Minimum required size is %dx%d.',
|
|
self::MIN_WIDTH,
|
|
self::MIN_HEIGHT,
|
|
));
|
|
}
|
|
|
|
$image = $this->manager->read($raw)->scaleDown(width: self::MAX_WIDTH, height: self::MAX_HEIGHT);
|
|
$encoded = (string) $image->encode(new WebpEncoder(85));
|
|
|
|
$hash = hash('sha256', $encoded);
|
|
$path = $this->mediaPath($hash);
|
|
$disk = Storage::disk($this->mediaDiskName());
|
|
|
|
$written = $disk->put($path, $encoded, [
|
|
'visibility' => 'public',
|
|
'CacheControl' => 'public, max-age=31536000, immutable',
|
|
'ContentType' => 'image/webp',
|
|
]);
|
|
|
|
if ($written !== true) {
|
|
throw new RuntimeException('Unable to store image in object storage.');
|
|
}
|
|
|
|
return [
|
|
'path' => $path,
|
|
'width' => (int) $image->width(),
|
|
'height' => (int) $image->height(),
|
|
'size_bytes' => strlen($encoded),
|
|
];
|
|
}
|
|
|
|
private function authorizeNews(Request $request): void
|
|
{
|
|
abort_unless($request->user() && ($request->user()->isAdmin() || $request->user()->isModerator()), 403);
|
|
}
|
|
} |