PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.0
ووسلام – همگام سازی ووکامرس و باسلام v1.10.0
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
sync-basalam / includes / Services / Api / RequestStatusTracker.php

RequestStatusTracker.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.0, at includes/Services/Api/RequestStatusTracker.php

512 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Services\Api;
4
5 defined('ABSPATH') || exit;
6
7 class RequestStatusTracker
8 {
9 private const OPTION_KEY = 'sync_basalam_request_status_stats';
10 private const RETENTION_SECONDS = 259200;
11 private const BUCKET_SECONDS = 3600;
12
13 public static function recordWpError(\WP_Error $response, string $url = ''): string
14 {
15 $errorCode = (string) $response->get_error_code();
16 $errorMessage = (string) $response->get_error_message();
17 $category = self::categorizeWpError($errorCode, $errorMessage);
18
19 self::recordEvent($category, [
20 'url' => $url,
21 'wp_error_code' => $errorCode,
22 ]);
23
24 return $category;
25 }
26
27 public static function recordHttpStatus(int $statusCode, string $url = ''): string
28 {
29 $category = self::categorizeHttpStatus($statusCode);
30
31 self::recordEvent($category, [
32 'url' => $url,
33 'status_code' => $statusCode,
34 ]);
35
36 return $category;
37 }
38
39 public static function recordCategory(string $category, array $context = []): void
40 {
41 self::recordEvent($category, $context);
42 }
43
44 /**
45 * Returns a user-friendly Persian explanation for a given error category.
46 * Intended for log messages so non-technical users can understand the cause.
47 */
48 public static function describeCategoryFa(string $category, ?int $statusCode = null): string
49 {
50 switch ($category) {
51 case 'success':
52 return 'درخواست با �
53 وفقیت انجا�
54 شد.';
55
56 case 'timeout':
57 return 'خطای تای�
58 اوت رخ داد و سرویس باسلا�
59 در ز�
60 ان �
61 قرر پاسخ نداد. �
62 ع�
63 ولاً به دلیل کندی �
64 وقت اینترنت سرور یا شلوغی سرویس باسلا�
65 رخ �
66 ی‌دهد و افزونه به‌صورت خودکار دوباره تلاش �
67 ی‌کند.';
68
69 case 'rate_limit':
70 return 'rate limit رخ داد و تعداد درخواست‌ها در بازه‌ی کوتاه از حد �
71 جاز عبور کرده است. افزونه برای دقایقی ریکوئستی به باسلا�
72 ارسال ن�
73 یکند و پس از دقایقی و دوباره تلاش �
74 ی‌کند.';
75
76 case 'unauthorized':
77 return 'توکن باسلا�
78
79 نقضی شده است. لطفاً از طریق تنظی�
80 ات افزونه دوباره به حساب باسلا�
81 خود وارد شوید.';
82
83 case 'server_error':
84 $code = $statusCode ? ' (کد ' . $statusCode . ')' : '';
85 return 'خطای داخلی در سرورهای باسلا�
86 رخ داده است' . $code . '. �
87 شکل از س�
88 ت افزونه نیست و �
89 ع�
90 ولاً پس از چند دقیقه برطرف �
91 ی‌شود.';
92
93 case 'client_error':
94 $code = $statusCode ? ' (کد ' . $statusCode . ')' : '';
95 return ' داده‌های ارسالی (payload) به باسلا�
96 صحیح نیست' . $code . 'در صورت نا�
97 فه�
98 و�
99 بودن خطا، لطفا تیکت ثبت کنید(https://admin.hamsalam.ir/ticket/new)';
100
101 case 'blocked_http':
102 return 'وردپرس درخواست‌های HTTP خارجی را �
103 سدود کرده است (تنظی�
104 WP_HTTP_BLOCK_EXTERNAL). برای رفع، دا�
105 نه basalam.com باید به WP_ACCESSIBLE_HOSTS اضافه شود.';
106
107 case 'dns_error':
108 return 'دا�
109 نه‌ی باسلا�
110 از روی سرور ش�
111 ا قابل دسترسی نیست. �
112 ع�
113 ولاً به دلیل تنظی�
114 ات DNS هاست یا �
115 حدودیت‌های شبکه‌ی سرور است.';
116
117 case 'ssl_error':
118 return 'برقراری اتصال ا�
119 ن (SSL/TLS) با باسلا�
120
121
122 کن نشد. �
123 ع�
124 ولاً به دلیل قدی�
125 ی بودن گواهی‌های root روی سرور هاست است.';
126
127 case 'connection_error':
128 return 'اتصال به سرور باسلا�
129 برقرار نشد یا در �
130 یانه‌ی راه قطع شد. �
131 شکل �
132 وقتی شبکه است و افزونه دوباره تلاش �
133 ی‌کند.';
134
135 case 'network_error':
136 return 'خطای ع�
137 و�
138 ی شبکه هنگا�
139 ارتباط با باسلا�
140 . �
141
142 کن است �
143 شکل از س�
144 ت سرور هاست یا فایروال باشد.';
145
146 case 'invalid_response':
147 return 'پاسخی �
148 عتبر از باسلا�
149 دریافت نشد (کد وضعیت نا�
150 شخص). �
151 ع�
152 ولاً به دلیل قطع شدن ارتباط در �
153 یانه‌ی پاسخ است.';
154
155 case 'unexpected_status':
156 $code = $statusCode ? ' (کد ' . $statusCode . ')' : '';
157 return 'وضعیت غیر�
158 نتظره‌ای از باسلا�
159 دریافت شد' . $code . '.';
160
161 case 'wp_error':
162 default:
163 return 'خطایی هنگا�
164 ارسال درخواست به باسلا�
165 رخ داد.';
166 }
167 }
168
169 /**
170 * Resolve a Persian reason directly from a WP_Error (categorizes internally).
171 */
172 public static function describeWpErrorFa(\WP_Error $error): string
173 {
174 $category = self::categorizeWpError(
175 (string) $error->get_error_code(),
176 (string) $error->get_error_message()
177 );
178
179 return self::describeCategoryFa($category);
180 }
181
182 /**
183 * Resolve a Persian reason directly from an HTTP status code (categorizes internally).
184 */
185 public static function describeHttpStatusFa(int $statusCode): string
186 {
187 return self::describeCategoryFa(self::categorizeHttpStatus($statusCode), $statusCode);
188 }
189
190 public static function getSummary(): array
191 {
192 $state = self::getState();
193 $aggregated = [
194 'total_requests' => 0,
195 'categories' => [],
196 'http_statuses' => [],
197 'wp_error_codes' => [],
198 'hosts' => [],
199 'hosts_by_category' => [],
200 ];
201
202 foreach ($state['buckets'] as $bucket) {
203 $aggregated['total_requests'] += (int) ($bucket['total_requests'] ?? 0);
204 self::mergeCounts($aggregated['categories'], $bucket['categories'] ?? []);
205 self::mergeCounts($aggregated['http_statuses'], $bucket['http_statuses'] ?? []);
206 self::mergeCounts($aggregated['wp_error_codes'], $bucket['wp_error_codes'] ?? []);
207 self::mergeCounts($aggregated['hosts'], $bucket['hosts'] ?? []);
208 self::mergeNestedCounts($aggregated['hosts_by_category'], $bucket['hosts_by_category'] ?? []);
209 }
210
211 arsort($aggregated['categories']);
212 arsort($aggregated['http_statuses']);
213 arsort($aggregated['wp_error_codes']);
214 arsort($aggregated['hosts']);
215
216 return [
217 'window_hours' => 72,
218 'retention_seconds' => self::RETENTION_SECONDS,
219 'collected_until' => gmdate('c'),
220 'tracking_since' => self::resolveTrackingSince($state['buckets']),
221 'total_requests' => $aggregated['total_requests'],
222 'categories' => $aggregated['categories'],
223 'http_statuses' => $aggregated['http_statuses'],
224 'wp_error_codes' => $aggregated['wp_error_codes'],
225 'hosts' => $aggregated['hosts'],
226 'hosts_by_category' => self::sortNestedCounts($aggregated['hosts_by_category']),
227 'bucket_count' => count($state['buckets']),
228 'storage_option' => self::OPTION_KEY,
229 'expires_before' => gmdate('c', time() - self::RETENTION_SECONDS),
230 ];
231 }
232
233 private static function categorizeWpError(string $errorCode, string $errorMessage): string
234 {
235 $haystack = strtolower($errorCode . ' ' . $errorMessage);
236
237 if (self::containsAny($haystack, [
238 'http_request_not_executed',
239 'blocked requests through http',
240 'blocked the http request',
241 'درخواست http را بلوکه',
242 'درخواست http توسط وردپرس �
243 سدود',
244 ])) {
245 return 'blocked_http';
246 }
247
248 if (self::containsAny($haystack, [
249 'timeout',
250 'timed out',
251 'operation timed out',
252 'curl error 28',
253 'connection timeout',
254 ])) {
255 return 'timeout';
256 }
257
258 if (self::containsAny($haystack, [
259 'dns',
260 'could not resolve host',
261 'couldn\'t resolve host',
262 'getaddrinfo',
263 'name or service not known',
264 'temporary failure in name resolution',
265 'curl error 6',
266 ])) {
267 return 'dns_error';
268 }
269
270 if (self::containsAny($haystack, [
271 'ssl',
272 'tls',
273 'certificate',
274 'cert',
275 'curl error 35',
276 'curl error 51',
277 'curl error 58',
278 'curl error 60',
279 ])) {
280 return 'ssl_error';
281 }
282
283 if (self::containsAny($haystack, [
284 'connection refused',
285 'failed to connect',
286 'connection reset',
287 'socket',
288 'proxy connect aborted',
289 'curl error 7',
290 'curl error 52',
291 'curl error 56',
292 ])) {
293 return 'connection_error';
294 }
295
296 if (self::containsAny($haystack, [
297 'network',
298 'connection',
299 'curl error',
300 ])) {
301 return 'network_error';
302 }
303
304 return 'wp_error';
305 }
306
307 private static function categorizeHttpStatus(int $statusCode): string
308 {
309 if (in_array($statusCode, [200, 201, 202], true)) {
310 return 'success';
311 }
312
313 if ($statusCode === 0) {
314 return 'invalid_response';
315 }
316
317 if (in_array($statusCode, [408, 504], true)) {
318 return 'timeout';
319 }
320
321 if ($statusCode === 429) {
322 return 'rate_limit';
323 }
324
325 if ($statusCode === 401) {
326 return 'unauthorized';
327 }
328
329 if (in_array($statusCode, [500, 502, 503], true)) {
330 return 'server_error';
331 }
332
333 if ($statusCode >= 400 && $statusCode < 500) {
334 return 'client_error';
335 }
336
337 if ($statusCode >= 500) {
338 return 'server_error';
339 }
340
341 return 'unexpected_status';
342 }
343
344 private static function recordEvent(string $category, array $context = []): void
345 {
346 $state = self::getState();
347 $bucketKey = self::getBucketKey();
348
349 if (!isset($state['buckets'][$bucketKey])) {
350 $state['buckets'][$bucketKey] = [
351 'total_requests' => 0,
352 'categories' => [],
353 'http_statuses' => [],
354 'wp_error_codes' => [],
355 'hosts' => [],
356 'hosts_by_category' => [],
357 ];
358 }
359
360 $state['buckets'][$bucketKey]['total_requests']++;
361 self::incrementCount($state['buckets'][$bucketKey]['categories'], $category);
362
363 if (array_key_exists('status_code', $context)) {
364 self::incrementCount($state['buckets'][$bucketKey]['http_statuses'], (string) $context['status_code']);
365 }
366
367 if (!empty($context['wp_error_code'])) {
368 self::incrementCount($state['buckets'][$bucketKey]['wp_error_codes'], (string) $context['wp_error_code']);
369 }
370
371 $host = self::extractHost($context['url'] ?? '');
372 if ($host !== null) {
373 self::incrementCount($state['buckets'][$bucketKey]['hosts'], $host);
374 if (!isset($state['buckets'][$bucketKey]['hosts_by_category'][$host])) {
375 $state['buckets'][$bucketKey]['hosts_by_category'][$host] = [];
376 }
377 self::incrementCount($state['buckets'][$bucketKey]['hosts_by_category'][$host], $category);
378 }
379
380 update_option(self::OPTION_KEY, $state, false);
381 }
382
383 private static function getState(): array
384 {
385 $state = get_option(self::OPTION_KEY, []);
386
387 if (!is_array($state)) {
388 $state = [];
389 }
390
391 $originalBuckets = $state['buckets'] ?? [];
392 $state['buckets'] = self::pruneBuckets($originalBuckets);
393
394 if ($state['buckets'] !== $originalBuckets) {
395 update_option(self::OPTION_KEY, $state, false);
396 }
397
398 return $state;
399 }
400
401 private static function pruneBuckets(array $buckets): array
402 {
403 $minTimestamp = time() - self::RETENTION_SECONDS;
404
405 foreach ($buckets as $timestamp => $bucket) {
406 if ((int) $timestamp < $minTimestamp) {
407 unset($buckets[$timestamp]);
408 }
409 }
410
411 ksort($buckets);
412
413 return $buckets;
414 }
415
416 private static function getBucketKey(): int
417 {
418 return (int) (floor(time() / self::BUCKET_SECONDS) * self::BUCKET_SECONDS);
419 }
420
421 private static function incrementCount(array &$counts, string $key): void
422 {
423 if (!isset($counts[$key])) {
424 $counts[$key] = 0;
425 }
426
427 $counts[$key]++;
428 }
429
430 private static function mergeCounts(array &$target, array $source): void
431 {
432 foreach ($source as $key => $count) {
433 if (!isset($target[$key])) {
434 $target[$key] = 0;
435 }
436
437 $target[$key] += (int) $count;
438 }
439 }
440
441 private static function mergeNestedCounts(array &$target, array $source): void
442 {
443 foreach ($source as $groupKey => $counts) {
444 if (!isset($target[$groupKey]) || !is_array($target[$groupKey])) {
445 $target[$groupKey] = [];
446 }
447
448 self::mergeCounts($target[$groupKey], is_array($counts) ? $counts : []);
449 }
450 }
451
452 private static function sortNestedCounts(array $nestedCounts): array
453 {
454 foreach ($nestedCounts as &$counts) {
455 if (is_array($counts)) {
456 arsort($counts);
457 }
458 }
459 unset($counts);
460
461 uksort($nestedCounts, static function ($left, $right) use ($nestedCounts) {
462 $leftTotal = array_sum($nestedCounts[$left] ?? []);
463 $rightTotal = array_sum($nestedCounts[$right] ?? []);
464
465 if ($leftTotal === $rightTotal) {
466 return strcmp((string) $left, (string) $right);
467 }
468
469 return $rightTotal <=> $leftTotal;
470 });
471
472 return $nestedCounts;
473 }
474
475 private static function resolveTrackingSince(array $buckets): ?string
476 {
477 $firstBucket = array_key_first($buckets);
478
479 if ($firstBucket === null) {
480 return null;
481 }
482
483 return gmdate('c', (int) $firstBucket);
484 }
485
486 private static function extractHost(string $url): ?string
487 {
488 if ($url === '') {
489 return null;
490 }
491
492 $host = parse_url($url, PHP_URL_HOST);
493
494 if (!is_string($host) || $host === '') {
495 return null;
496 }
497
498 return strtolower($host);
499 }
500
501 private static function containsAny(string $haystack, array $needles): bool
502 {
503 foreach ($needles as $needle) {
504 if (strpos($haystack, strtolower($needle)) !== false) {
505 return true;
506 }
507 }
508
509 return false;
510 }
511 }
512