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); } }