| @@ -1,66 +1,170 @@ | ||
| 1 | -<?php | |
| 2 | - | |
| 3 | -namespace SyncBasalam\Services\Api; | |
| 4 | - | |
| 5 | -use SyncBasalam\Logger\Logger; | |
| 6 | - | |
| 7 | -defined('ABSPATH') || exit; | |
| 8 | - | |
| 9 | -class FileUploadApiService | |
| 10 | -{ | |
| 11 | - public function upload(string $url, string $filePath, array $data = [], array $headers = []): array | |
| 12 | - { | |
| 13 | - $fileValidation = $this->validateFile($filePath); | |
| 14 | - if (!$fileValidation['valid']) { | |
| 15 | - return [ | |
| 16 | - 'body' => null, | |
| 17 | - 'status_code' => 400, | |
| 18 | - 'error' => $fileValidation['message'] | |
| 19 | - ]; | |
| 20 | - } | |
| 21 | - | |
| 22 | - $boundary = wp_generate_password(24); | |
| 23 | - $payload = $this->makePayloadupload($data, $filePath, $boundary); | |
| 24 | - | |
| 25 | - $headers = array_merge($headers, ['content-type' => 'multipart/form-data; boundary=' . $boundary]); | |
| 26 | - | |
| 27 | - $response = wp_remote_post($url, ['headers' => $headers, 'body' => $payload]); | |
| 28 | - | |
| 1 | +<?php | |
| 2 | + | |
| 3 | +namespace SyncBasalam\Services\Api; | |
| 4 | + | |
| 5 | +use SyncBasalam\Logger\Logger; | |
| 6 | +use SyncBasalam\Services\MediaMimeType; | |
| 7 | + | |
| 8 | +defined('ABSPATH') || exit; | |
| 9 | + | |
| 10 | +class FileUploadApiService | |
| 11 | +{ | |
| 12 | + public function upload(string $url, string $filePath, array $data = [], array $headers = [], array $options = []): array | |
| 13 | + { | |
| 14 | + $fileValidation = $this->validateFile($filePath, $options); | |
| 15 | + if (!$fileValidation['valid']) { | |
| 16 | + return [ | |
| 17 | + 'body' => null, | |
| 18 | + 'status_code' => 400, | |
| 19 | + 'error' => $fileValidation['message'] | |
| 20 | + ]; | |
| 21 | + } | |
| 22 | + | |
| 23 | + if (function_exists('curl_init') && class_exists('CURLFile')) { | |
| 24 | + return $this->uploadWithCurl($url, $filePath, $data, $headers, $options); | |
| 25 | + } | |
| 26 | + | |
| 27 | + $fileSize = filesize($filePath); | |
| 28 | + if ($fileSize !== false && $fileSize > 10 * 1024 * 1024) { | |
| 29 | + return [ | |
| 30 | + 'body' => null, | |
| 31 | + 'status_code' => 500, | |
| 32 | + 'error' => 'برای آپلود فایلهای بزرگ، افزونه cURL در PHP باید فعال باشد.', | |
| 33 | + ]; | |
| 34 | + } | |
| 35 | + | |
| 36 | + $boundary = wp_generate_password(24); | |
| 37 | + $payload = $this->makePayloadupload($data, $filePath, $boundary); | |
| 38 | + $headers = array_merge($headers, ['content-type' => 'multipart/form-data; boundary=' . $boundary]); | |
| 39 | + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : 120; | |
| 40 | + | |
| 41 | + $response = wp_remote_post($url, [ | |
| 42 | + 'headers' => $headers, | |
| 43 | + 'body' => $payload, | |
| 44 | + 'timeout' => max(10, $timeout), | |
| 45 | + ]); | |
| 46 | + | |
| 29 | 47 | return $this->handleResponse($response, $url); |
| 30 | - } | |
| 31 | - | |
| 32 | - private function validateFile(string $filePath): array | |
| 33 | - { | |
| 34 | - if (!file_exists($filePath)) { | |
| 35 | - return ['valid' => false, 'message' => 'فایل در دسترس نیست' . $filePath]; | |
| 36 | - } | |
| 37 | - | |
| 38 | - $fileSize = filesize($filePath); | |
| 39 | - if ($fileSize > 5 * 1024 * 1024) { | |
| 40 | - return ['valid' => false, 'message' => 'حجم فایل بیش از حد مجاز است : ' . $filePath]; | |
| 41 | - } | |
| 42 | - | |
| 43 | - $allowedExtensions = ['jpg', 'png', 'webp', 'bmp', 'jfif', 'jpeg', 'avif']; | |
| 44 | - $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); | |
| 45 | - | |
| 46 | - if (!in_array($extension, $allowedExtensions)) { | |
| 47 | - return ['valid' => false, 'message' => 'فرمت تصویر متغیر نیست ، فرمت های معتبر : ' . implode(', ', $allowedExtensions)]; | |
| 48 | - } | |
| 49 | - | |
| 50 | - return ['valid' => true, 'message' => 'فایل متعبر است']; | |
| 51 | - } | |
| 52 | - | |
| 48 | + } | |
| 49 | + | |
| 50 | + private function uploadWithCurl(string $url, string $filePath, array $data, array $headers, array $options): array | |
| 51 | + { | |
| 52 | + $mimeType = MediaMimeType::detect($filePath); | |
| 53 | + $fields = $data; | |
| 54 | + $fields['file'] = new \CURLFile($filePath, $mimeType, basename($filePath)); | |
| 55 | + $headerLines = []; | |
| 56 | + | |
| 57 | + foreach ($headers as $name => $value) { | |
| 58 | + if (strtolower((string) $name) === 'content-type') continue; | |
| 59 | + $headerLines[] = $name . ': ' . $value; | |
| 60 | + } | |
| 61 | + | |
| 62 | + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : 120; | |
| 63 | + $curl = curl_init($url); | |
| 64 | + | |
| 65 | + curl_setopt_array($curl, [ | |
| 66 | + CURLOPT_POST => true, | |
| 67 | + CURLOPT_POSTFIELDS => $fields, | |
| 68 | + CURLOPT_HTTPHEADER => $headerLines, | |
| 69 | + CURLOPT_RETURNTRANSFER => true, | |
| 70 | + CURLOPT_CONNECTTIMEOUT => 20, | |
| 71 | + CURLOPT_TIMEOUT => max(10, $timeout), | |
| 72 | + CURLOPT_FOLLOWLOCATION => false, | |
| 73 | + CURLOPT_SSL_VERIFYPEER => true, | |
| 74 | + CURLOPT_SSL_VERIFYHOST => 2, | |
| 75 | + ]); | |
| 76 | + | |
| 77 | + $body = curl_exec($curl); | |
| 78 | + $statusCode = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE); | |
| 79 | + | |
| 80 | + if ($body === false) { | |
| 81 | + $message = curl_error($curl); | |
| 82 | + curl_close($curl); | |
| 83 | + | |
| 84 | + Logger::error('خطا در ارسال فایل با cURL: ' . $message, ['url' => $url]); | |
| 85 | + | |
| 86 | + return [ | |
| 87 | + 'body' => null, | |
| 88 | + 'status_code' => 500, | |
| 89 | + 'error' => $message ?: 'خطای نامشخص در ارسال فایل', | |
| 90 | + ]; | |
| 91 | + } | |
| 92 | + | |
| 93 | + curl_close($curl); | |
| 94 | + RequestStatusTracker::recordHttpStatus($statusCode, $url); | |
| 95 | + $decodedBody = json_decode($body, true); | |
| 96 | + | |
| 97 | + if ($statusCode >= 400) { | |
| 98 | + $reason = RequestStatusTracker::describeHttpStatusFa($statusCode); | |
| 99 | + Logger::error('آپلود فایل به باسلام ناموفق بود. ' . $reason, [ | |
| 100 | + 'url' => $url, | |
| 101 | + 'status_code' => $statusCode, | |
| 102 | + 'reason' => $reason, | |
| 103 | + 'response_body' => $decodedBody ?? $body, | |
| 104 | + ]); | |
| 105 | + } | |
| 106 | + | |
| 107 | + return [ | |
| 108 | + 'body' => $decodedBody, | |
| 109 | + 'status_code' => $statusCode, | |
| 110 | + 'error' => $statusCode >= 400 ? $this->extractErrorMessage($decodedBody, $body) : null, | |
| 111 | + ]; | |
| 112 | + } | |
| 113 | + | |
| 114 | + private function validateFile(string $filePath, array $options = []): array | |
| 115 | + { | |
| 116 | + if (!file_exists($filePath)) { | |
| 117 | + return ['valid' => false, 'message' => 'فایل در دسترس نیست' . $filePath]; | |
| 118 | + } | |
| 119 | + | |
| 120 | + $maxSize = isset($options['max_size']) ? (int) $options['max_size'] : 5 * 1024 * 1024; | |
| 121 | + | |
| 122 | + $fileSize = filesize($filePath); | |
| 123 | + if ($fileSize > $maxSize) { | |
| 124 | + return ['valid' => false, 'message' => 'حجم فایل بیش از حد مجاز است : ' . $filePath]; | |
| 125 | + } | |
| 126 | + | |
| 127 | + $allowedExtensions = isset($options['allowed_extensions']) && is_array($options['allowed_extensions']) | |
| 128 | + ? $options['allowed_extensions'] | |
| 129 | + : ['jpg', 'png', 'webp', 'bmp', 'jfif', 'jpeg', 'avif']; | |
| 130 | + | |
| 131 | + $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); | |
| 132 | + | |
| 133 | + if (!in_array($extension, $allowedExtensions, true)) { | |
| 134 | + return ['valid' => false, 'message' => 'فرمت فایل معتبر نیست ، فرمت های معتبر : ' . implode(', ', $allowedExtensions)]; | |
| 135 | + } | |
| 136 | + | |
| 137 | + return ['valid' => true, 'message' => 'فایل معتبر است']; | |
| 138 | + } | |
| 139 | + | |
| 53 | 140 | private function handleResponse($response, string $url = ''): array |
| 54 | 141 | { |
| 55 | 142 | if (is_wp_error($response)) { |
| 56 | - RequestStatusTracker::recordWpError($response, $url); | |
| 143 | + $category = RequestStatusTracker::recordWpError($response, $url); | |
| 144 | + $errorData = $response->get_error_data(); | |
| 145 | + $errorCodes = $response->get_error_codes(); | |
| 146 | + $errorMessages = []; | |
| 57 | 147 | |
| 148 | + foreach ($errorCodes as $code) { | |
| 149 | + $messages = $response->get_error_messages($code); | |
| 150 | + if (!empty($messages)) $errorMessages[$code] = $messages; | |
| 151 | + } | |
| 152 | + | |
| 153 | + Logger::error('خطا در فرایند آپلود فایل در باسلام. ' . RequestStatusTracker::describeCategoryFa($category), [ | |
| 154 | + 'url' => $url, | |
| 155 | + 'category' => $category, | |
| 156 | + 'reason' => RequestStatusTracker::describeCategoryFa($category), | |
| 157 | + 'error_codes' => $errorCodes, | |
| 158 | + 'error_messages' => $errorMessages, | |
| 159 | + 'error_data' => $errorData, | |
| 160 | + ]); | |
| 161 | + | |
| 58 | 162 | return [ |
| 59 | 163 | 'body' => null, |
| 60 | - 'status_code' => 500, | |
| 61 | - 'error' => $response->get_error_message() | |
| 62 | - ]; | |
| 164 | + 'status_code' => $this->resolveWpErrorStatusCode($response), | |
| 165 | + 'error' => $this->resolveWpErrorMessage($response) | |
| 166 | + ]; | |
| 63 | 167 | } |
| 64 | 168 | |
| 65 | 169 | $body = wp_remote_retrieve_body($response); |
| 66 | 170 | $statusCode = wp_remote_retrieve_response_code($response); |
| @@ -67,46 +171,91 @@ | ||
| 67 | 171 | RequestStatusTracker::recordHttpStatus((int) $statusCode, $url); |
| 68 | 172 | |
| 69 | 173 | $decodedBody = json_decode($body, true); |
| 70 | 174 | |
| 175 | + if ((int) $statusCode >= 400) { | |
| 176 | + $reason = RequestStatusTracker::describeHttpStatusFa((int) $statusCode); | |
| 177 | + Logger::error('آپلود فایل به باسلام ناموفق بود. ' . $reason, [ | |
| 178 | + 'url' => $url, | |
| 179 | + 'status_code' => (int) $statusCode, | |
| 180 | + 'reason' => $reason, | |
| 181 | + 'response_body' => $decodedBody ?? $body, | |
| 182 | + ]); | |
| 183 | + } | |
| 184 | + | |
| 71 | 185 | return [ |
| 72 | 186 | 'body' => $decodedBody, |
| 73 | - 'status_code' => $statusCode, | |
| 74 | - 'error' => null | |
| 75 | - ]; | |
| 76 | - } | |
| 77 | - | |
| 78 | - private function makePayloadupload(array $data, string $localFile, string $boundary): string | |
| 79 | - { | |
| 80 | - $payload = ''; | |
| 81 | - $eol = "\r\n"; | |
| 82 | - | |
| 83 | - foreach ($data as $name => $value) { | |
| 84 | - $payload .= '--' . $boundary . $eol; | |
| 85 | - $payload .= 'Content-Disposition: form-data; name="' . $name . '"' . $eol . $eol; | |
| 86 | - $payload .= $value . $eol; | |
| 87 | - } | |
| 88 | - | |
| 89 | - if ($localFile) { | |
| 90 | - $filename = basename($localFile); | |
| 91 | - $fileContent = file_get_contents($localFile); | |
| 92 | - | |
| 93 | - if ($fileContent === false) { | |
| 94 | - Logger::error("خطا در خواندن فایل با استفاده از file_get_contents: " . $localFile); | |
| 95 | - return ''; | |
| 96 | - } | |
| 97 | - | |
| 98 | - $filetypeInfo = wp_check_filetype($localFile); | |
| 99 | - $mimeType = $filetypeInfo['type'] ?? 'application/octet-stream'; | |
| 100 | - | |
| 101 | - $payload .= '--' . $boundary . $eol; | |
| 102 | - $payload .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol; | |
| 103 | - $payload .= 'Content-Type: ' . $mimeType . $eol; | |
| 104 | - $payload .= 'Content-Transfer-Encoding: binary' . $eol . $eol; | |
| 105 | - $payload .= $fileContent . $eol; | |
| 106 | - } | |
| 107 | - | |
| 108 | - $payload .= '--' . $boundary . '--' . $eol; | |
| 109 | - | |
| 110 | - return $payload; | |
| 111 | - } | |
| 112 | -} | |
| 187 | + 'status_code' => $statusCode, | |
| 188 | + 'error' => (int) $statusCode >= 400 ? $this->extractErrorMessage($decodedBody, $body) : null | |
| 189 | + ]; | |
| 190 | + } | |
| 191 | + | |
| 192 | + private function makePayloadupload(array $data, string $localFile, string $boundary): string | |
| 193 | + { | |
| 194 | + $payload = ''; | |
| 195 | + $eol = "\r\n"; | |
| 196 | + | |
| 197 | + foreach ($data as $name => $value) { | |
| 198 | + $payload .= '--' . $boundary . $eol; | |
| 199 | + $payload .= 'Content-Disposition: form-data; name="' . $name . '"' . $eol . $eol; | |
| 200 | + $payload .= $value . $eol; | |
| 201 | + } | |
| 202 | + | |
| 203 | + if ($localFile) { | |
| 204 | + $filename = basename($localFile); | |
| 205 | + $fileContent = file_get_contents($localFile); | |
| 206 | + | |
| 207 | + if ($fileContent === false) { | |
| 208 | + Logger::error('خطا در خواندن فایل با استفاده از file_get_contents: ' . $localFile); | |
| 209 | + return ''; | |
| 210 | + } | |
| 211 | + | |
| 212 | + $mimeType = MediaMimeType::detect($localFile); | |
| 213 | + | |
| 214 | + $payload .= '--' . $boundary . $eol; | |
| 215 | + $payload .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol; | |
| 216 | + $payload .= 'Content-Type: ' . $mimeType . $eol; | |
| 217 | + $payload .= 'Content-Transfer-Encoding: binary' . $eol . $eol; | |
| 218 | + $payload .= $fileContent . $eol; | |
| 219 | + } | |
| 220 | + | |
| 221 | + $payload .= '--' . $boundary . '--' . $eol; | |
| 222 | + | |
| 223 | + return $payload; | |
| 224 | + } | |
| 225 | + | |
| 226 | + private function resolveWpErrorMessage(\WP_Error $response): string | |
| 227 | + { | |
| 228 | + $errorData = $response->get_error_data(); | |
| 229 | + $host = is_array($errorData) ? ($errorData['host'] ?? '') : ''; | |
| 230 | + $message = $response->get_error_message(); | |
| 231 | + | |
| 232 | + if ($response->get_error_code() === 'http_request_blocked') { | |
| 233 | + return 'درخواست آپلود تصویر به باسلام مسدود شد' . ($host ? ': ' . $host : ''); | |
| 234 | + } | |
| 235 | + | |
| 236 | + return $message ?: 'خطای نامشخص در آپلود فایل'; | |
| 237 | + } | |
| 238 | + | |
| 239 | + private function resolveWpErrorStatusCode(\WP_Error $response): int | |
| 240 | + { | |
| 241 | + $errorData = $response->get_error_data(); | |
| 242 | + | |
| 243 | + if (is_array($errorData) && isset($errorData['status']) && is_numeric($errorData['status'])) { | |
| 244 | + return (int) $errorData['status']; | |
| 245 | + } | |
| 246 | + | |
| 247 | + return 500; | |
| 248 | + } | |
| 249 | + | |
| 250 | + private function extractErrorMessage($decodedBody, string $rawBody): string | |
| 251 | + { | |
| 252 | + if (is_array($decodedBody)) { | |
| 253 | + if (!empty($decodedBody['messages'][0]['message'])) return (string) $decodedBody['messages'][0]['message']; | |
| 254 | + if (!empty($decodedBody['errors'][0]['message'])) return (string) $decodedBody['errors'][0]['message']; | |
| 255 | + if (!empty($decodedBody['error'])) return (string) $decodedBody['error']; | |
| 256 | + if (!empty($decodedBody['message'])) return (string) $decodedBody['message']; | |
| 257 | + } | |
| 258 | + | |
| 259 | + return $rawBody !== '' ? $rawBody : 'خطای نامشخص در آپلود فایل'; | |
| 260 | + } | |
| 261 | +} | |