Files
mystuff_backend/api/services/ImageHandler.php
Lars Behrends e38a6e1f7b Add strict types and type hints across API
Apply strict_types and extensive type declarations throughout the API and models, improving type safety and error handling. Key changes: add declare(strict_types=1) to many files; convert properties, method parameters and return values to typed signatures (PDO, arrays, ints, strings, bools, nullables); switch exception handling to Throwable in index and Router; improve Router, controllers and model method signatures and nullability handling; refine file/image serving security checks and headers in ImageController; strengthen Database typing and initialization methods; return explicit types from BaseModel CRUD helpers and counting; update Media/Cast/Adult/Game/Console/Settings controllers and models to use typed methods, better validation, and clearer update/create return types. Also add AGENTS.md (agent skills index), update README with Swagger/OpenAPI usage instructions, and add /.windsurf to .gitignore. These changes aim to harden runtime correctness, make intended contracts explicit, and prepare the codebase for easier maintenance and static analysis.
2026-04-16 16:40:31 +02:00

186 lines
6.6 KiB
PHP

<?php
declare(strict_types=1);
class ImageHandler {
private string $uploadDir;
private string $baseUrl;
public function __construct(?string $uploadDir = null, ?string $baseUrl = null) {
$this->uploadDir = $uploadDir ?? __DIR__ . '/../public/images/';
$this->baseUrl = $baseUrl ?? '/images/';
// Ensure upload directory exists
if (!file_exists($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
/**
* Process base64 image data and save to file
*
* @param string $base64Data Base64 encoded image data
* @param string $prefix Prefix for filename (e.g., 'poster', 'banner')
* @return string|null Relative path to saved image, or null if invalid
*/
public function saveBase64Image(string $base64Data, string $prefix = 'image'): ?string {
error_log("ImageHandler: Starting to process base64 image, length: " . strlen($base64Data));
if (empty($base64Data)) {
error_log("ImageHandler: Empty base64 data");
return null;
}
// Check if it's already a URL (not base64)
if (filter_var($base64Data, FILTER_VALIDATE_URL)) {
error_log("ImageHandler: Data is already a URL: " . $base64Data);
return $base64Data;
}
// Check if it's already a file path
if (strpos($base64Data, '/images/') === 0) {
error_log("ImageHandler: Data is already a file path: " . $base64Data);
return $base64Data;
}
// Parse base64 data
if (preg_match('/^data:image\/(\w+);base64,(.+)$/', $base64Data, $matches)) {
$extension = $matches[1];
$base64String = $matches[2];
error_log("ImageHandler: Data URI format detected, extension: " . $extension);
} elseif (preg_match('/^\/9j\//', $base64Data)) {
// Raw base64 without data URI prefix (JPEG)
$extension = 'jpg';
$base64String = $base64Data;
error_log("ImageHandler: Raw JPEG base64 detected");
} else {
// Try to detect from the base64 string itself
$extension = $this->detectImageFormat($base64Data);
if (!$extension) {
error_log("ImageHandler: Could not detect image format, defaulting to jpg");
$extension = 'jpg';
$base64String = $base64Data;
} else {
$base64String = $base64Data;
error_log("ImageHandler: Detected format: " . $extension);
}
}
// Decode base64
$imageData = base64_decode($base64String);
if ($imageData === false) {
error_log("ImageHandler: Base64 decode failed");
return null;
}
error_log("ImageHandler: Decoded image data size: " . strlen($imageData) . " bytes");
// Skip GD validation - just check if data looks reasonable
if (strlen($imageData) < 100) {
error_log("ImageHandler: Image data too small, likely invalid");
return null;
}
// Generate unique filename
$filename = $this->generateUniqueFilename($prefix, $extension);
$filepath = $this->uploadDir . $filename;
error_log("ImageHandler: Attempting to save to: " . $filepath);
// Ensure directory exists and is writable (handle subdirectories)
$directory = dirname($filepath);
if (!file_exists($directory)) {
error_log("ImageHandler: Creating directory: " . $directory);
if (!mkdir($directory, 0755, true)) {
error_log("ImageHandler: Failed to create directory");
return null;
}
}
if (!is_writable($directory)) {
error_log("ImageHandler: Directory not writable: " . $directory);
error_log("ImageHandler: Attempting to chmod directory");
chmod($directory, 0755);
}
// Save file
$bytesWritten = file_put_contents($filepath, $imageData);
if ($bytesWritten === false) {
error_log("ImageHandler: Failed to save file to: " . $filepath);
error_log("ImageHandler: Upload directory exists: " . (file_exists($this->uploadDir) ? 'yes' : 'no'));
error_log("ImageHandler: Upload directory writable: " . (is_writable($this->uploadDir) ? 'yes' : 'no'));
return null;
}
error_log("ImageHandler: Successfully saved " . $bytesWritten . " bytes to: " . $filepath);
// Return relative path
return $this->baseUrl . $filename;
}
/**
* Detect image format from base64 string
*/
private function detectImageFormat(string $base64String): ?string {
// Decode first few bytes to check magic numbers
$data = base64_decode(substr($base64String, 0, 100));
if (substr($data, 0, 3) === "\xFF\xD8\xFF") {
return 'jpg';
} elseif (substr($data, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") {
return 'png';
} elseif (substr($data, 0, 6) === "GIF87a" || substr($data, 0, 6) === "GIF89a") {
return 'gif';
} elseif (substr($data, 0, 4) === "RIFF" && substr($data, 8, 4) === "WEBP") {
return 'webp';
}
// Default to jpg for game posters
return 'jpg';
}
/**
* Validate that data is a valid image
*/
private function isValidImage(string $data): bool {
try {
$image = imagecreatefromstring($data);
if ($image !== false) {
imagedestroy($image);
return true;
}
} catch (Exception $e) {
return false;
}
return false;
}
/**
* Generate unique filename
*/
private function generateUniqueFilename(string $prefix, string $extension): string {
return $prefix . '_' . uniqid() . '_' . time() . '.' . $extension;
}
/**
* Delete an image file
*/
public function deleteImage(?string $imagePath): bool {
if (empty($imagePath)) {
return false;
}
// Convert URL to filesystem path
if (strpos($imagePath, $this->baseUrl) === 0) {
$filename = substr($imagePath, strlen($this->baseUrl));
$filepath = $this->uploadDir . $filename;
if (file_exists($filepath)) {
return unlink($filepath);
}
}
return false;
}
}