'jpg', 'image/png' => 'png', 'image/webp' => 'webp', ]; public function __construct( private readonly DirectoryList $directoryList, private readonly File $fileDriver ) { } public function saveUploads(array $files, int $threadId, int $entryId): array { $uploads = $this->normalize($files); if (count($uploads) > self::MAX_FILES) { throw new LocalizedException(__('You can upload up to %1 images per message.', self::MAX_FILES)); } $saved = []; try { foreach ($uploads as $upload) { if ((int)$upload['error'] === UPLOAD_ERR_NO_FILE) { continue; } if ((int)$upload['error'] !== UPLOAD_ERR_OK) { throw new LocalizedException(__('One of the images could not be uploaded.')); } if ((int)$upload['size'] <= 0 || (int)$upload['size'] > self::MAX_SIZE) { throw new LocalizedException(__('Each image must be no larger than 5 MB.')); } $mime = (new \finfo(FILEINFO_MIME_TYPE))->file((string)$upload['tmp_name']); if (!isset(self::MIME_EXTENSIONS[$mime])) { throw new LocalizedException(__('Only JPG, PNG and WebP images are allowed.')); } $relativeDirectory = sprintf('esun_customer_message/%d/%d', $threadId, $entryId); $absoluteDirectory = $this->directoryList->getPath(DirectoryList::VAR_DIR) . '/' . $relativeDirectory; $this->fileDriver->createDirectory($absoluteDirectory, 0750); $filename = bin2hex(random_bytes(20)) . '.' . self::MIME_EXTENSIONS[$mime]; $absolutePath = $absoluteDirectory . '/' . $filename; if (!move_uploaded_file((string)$upload['tmp_name'], $absolutePath)) { throw new LocalizedException(__('The image could not be stored.')); } chmod($absolutePath, 0640); $saved[] = [ 'original_name' => mb_substr(basename((string)$upload['name']), 0, 255), 'storage_path' => $relativeDirectory . '/' . $filename, 'mime_type' => $mime, 'file_size' => (int)$upload['size'], ]; } } catch (\Throwable $exception) { $this->delete(array_column($saved, 'storage_path')); throw $exception; } return $saved; } public function hasUploads(array $files): bool { foreach ($this->normalize($files) as $upload) { if ((int)$upload['error'] !== UPLOAD_ERR_NO_FILE) { return true; } } return false; } public function getAbsolutePath(string $relativePath): string { $base = $this->directoryList->getPath(DirectoryList::VAR_DIR) . '/esun_customer_message'; $path = $this->directoryList->getPath(DirectoryList::VAR_DIR) . '/' . ltrim($relativePath, '/'); $realBase = realpath($base); $realPath = realpath($path); if ($realBase === false || $realPath === false || !str_starts_with($realPath, $realBase . DIRECTORY_SEPARATOR)) { throw new LocalizedException(__('The requested image is not available.')); } return $realPath; } public function delete(array $relativePaths): void { foreach ($relativePaths as $relativePath) { try { $path = $this->getAbsolutePath((string)$relativePath); if ($this->fileDriver->isExists($path)) { $this->fileDriver->deleteFile($path); } } catch (\Throwable) { // Best-effort cleanup after a failed transaction. } } } private function normalize(array $files): array { if (!$files) { return []; } if (isset($files['name']) && is_array($files['name'])) { $result = []; foreach ($files['name'] as $index => $name) { $result[] = [ 'name' => $name, 'type' => $files['type'][$index] ?? '', 'tmp_name' => $files['tmp_name'][$index] ?? '', 'error' => $files['error'][$index] ?? UPLOAD_ERR_NO_FILE, 'size' => $files['size'][$index] ?? 0, ]; } return $result; } if (isset($files['name'])) { return [$files]; } return array_values(array_filter($files, 'is_array')); } }