PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.9
ووسلام – همگام سازی ووکامرس و باسلام v1.10.9
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.9, at includes/Services/Api/RequestStatusTracker.php

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