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

366 lines 10.7 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 public static function getSummary(): array
45 {
46 $state = self::getState();
47 $aggregated = [
48 'total_requests' => 0,
49 'categories' => [],
50 'http_statuses' => [],
51 'wp_error_codes' => [],
52 'hosts' => [],
53 'hosts_by_category' => [],
54 ];
55
56 foreach ($state['buckets'] as $bucket) {
57 $aggregated['total_requests'] += (int) ($bucket['total_requests'] ?? 0);
58 self::mergeCounts($aggregated['categories'], $bucket['categories'] ?? []);
59 self::mergeCounts($aggregated['http_statuses'], $bucket['http_statuses'] ?? []);
60 self::mergeCounts($aggregated['wp_error_codes'], $bucket['wp_error_codes'] ?? []);
61 self::mergeCounts($aggregated['hosts'], $bucket['hosts'] ?? []);
62 self::mergeNestedCounts($aggregated['hosts_by_category'], $bucket['hosts_by_category'] ?? []);
63 }
64
65 arsort($aggregated['categories']);
66 arsort($aggregated['http_statuses']);
67 arsort($aggregated['wp_error_codes']);
68 arsort($aggregated['hosts']);
69
70 return [
71 'window_hours' => 72,
72 'retention_seconds' => self::RETENTION_SECONDS,
73 'collected_until' => gmdate('c'),
74 'tracking_since' => self::resolveTrackingSince($state['buckets']),
75 'total_requests' => $aggregated['total_requests'],
76 'categories' => $aggregated['categories'],
77 'http_statuses' => $aggregated['http_statuses'],
78 'wp_error_codes' => $aggregated['wp_error_codes'],
79 'hosts' => $aggregated['hosts'],
80 'hosts_by_category' => self::sortNestedCounts($aggregated['hosts_by_category']),
81 'bucket_count' => count($state['buckets']),
82 'storage_option' => self::OPTION_KEY,
83 'expires_before' => gmdate('c', time() - self::RETENTION_SECONDS),
84 ];
85 }
86
87 private static function categorizeWpError(string $errorCode, string $errorMessage): string
88 {
89 $haystack = strtolower($errorCode . ' ' . $errorMessage);
90
91 if (self::containsAny($haystack, [
92 'http_request_not_executed',
93 'blocked requests through http',
94 'blocked the http request',
95 'درخواست http را بلوکه',
96 'درخواست http توسط وردپرس �
97 سدود',
98 ])) {
99 return 'blocked_http';
100 }
101
102 if (self::containsAny($haystack, [
103 'timeout',
104 'timed out',
105 'operation timed out',
106 'curl error 28',
107 'connection timeout',
108 ])) {
109 return 'timeout';
110 }
111
112 if (self::containsAny($haystack, [
113 'dns',
114 'could not resolve host',
115 'couldn\'t resolve host',
116 'getaddrinfo',
117 'name or service not known',
118 'temporary failure in name resolution',
119 'curl error 6',
120 ])) {
121 return 'dns_error';
122 }
123
124 if (self::containsAny($haystack, [
125 'ssl',
126 'tls',
127 'certificate',
128 'cert',
129 'curl error 35',
130 'curl error 51',
131 'curl error 58',
132 'curl error 60',
133 ])) {
134 return 'ssl_error';
135 }
136
137 if (self::containsAny($haystack, [
138 'connection refused',
139 'failed to connect',
140 'connection reset',
141 'socket',
142 'proxy connect aborted',
143 'curl error 7',
144 'curl error 52',
145 'curl error 56',
146 ])) {
147 return 'connection_error';
148 }
149
150 if (self::containsAny($haystack, [
151 'network',
152 'connection',
153 'curl error',
154 ])) {
155 return 'network_error';
156 }
157
158 return 'wp_error';
159 }
160
161 private static function categorizeHttpStatus(int $statusCode): string
162 {
163 if (in_array($statusCode, [200, 201, 202], true)) {
164 return 'success';
165 }
166
167 if ($statusCode === 0) {
168 return 'invalid_response';
169 }
170
171 if (in_array($statusCode, [408, 504], true)) {
172 return 'timeout';
173 }
174
175 if ($statusCode === 429) {
176 return 'rate_limit';
177 }
178
179 if ($statusCode === 401) {
180 return 'unauthorized';
181 }
182
183 if (in_array($statusCode, [500, 502, 503], true)) {
184 return 'server_error';
185 }
186
187 if ($statusCode >= 400 && $statusCode < 500) {
188 return 'client_error';
189 }
190
191 if ($statusCode >= 500) {
192 return 'server_error';
193 }
194
195 return 'unexpected_status';
196 }
197
198 private static function recordEvent(string $category, array $context = []): void
199 {
200 $state = self::getState();
201 $bucketKey = self::getBucketKey();
202
203 if (!isset($state['buckets'][$bucketKey])) {
204 $state['buckets'][$bucketKey] = [
205 'total_requests' => 0,
206 'categories' => [],
207 'http_statuses' => [],
208 'wp_error_codes' => [],
209 'hosts' => [],
210 'hosts_by_category' => [],
211 ];
212 }
213
214 $state['buckets'][$bucketKey]['total_requests']++;
215 self::incrementCount($state['buckets'][$bucketKey]['categories'], $category);
216
217 if (array_key_exists('status_code', $context)) {
218 self::incrementCount($state['buckets'][$bucketKey]['http_statuses'], (string) $context['status_code']);
219 }
220
221 if (!empty($context['wp_error_code'])) {
222 self::incrementCount($state['buckets'][$bucketKey]['wp_error_codes'], (string) $context['wp_error_code']);
223 }
224
225 $host = self::extractHost($context['url'] ?? '');
226 if ($host !== null) {
227 self::incrementCount($state['buckets'][$bucketKey]['hosts'], $host);
228 if (!isset($state['buckets'][$bucketKey]['hosts_by_category'][$host])) {
229 $state['buckets'][$bucketKey]['hosts_by_category'][$host] = [];
230 }
231 self::incrementCount($state['buckets'][$bucketKey]['hosts_by_category'][$host], $category);
232 }
233
234 update_option(self::OPTION_KEY, $state, false);
235 }
236
237 private static function getState(): array
238 {
239 $state = get_option(self::OPTION_KEY, []);
240
241 if (!is_array($state)) {
242 $state = [];
243 }
244
245 $originalBuckets = $state['buckets'] ?? [];
246 $state['buckets'] = self::pruneBuckets($originalBuckets);
247
248 if ($state['buckets'] !== $originalBuckets) {
249 update_option(self::OPTION_KEY, $state, false);
250 }
251
252 return $state;
253 }
254
255 private static function pruneBuckets(array $buckets): array
256 {
257 $minTimestamp = time() - self::RETENTION_SECONDS;
258
259 foreach ($buckets as $timestamp => $bucket) {
260 if ((int) $timestamp < $minTimestamp) {
261 unset($buckets[$timestamp]);
262 }
263 }
264
265 ksort($buckets);
266
267 return $buckets;
268 }
269
270 private static function getBucketKey(): int
271 {
272 return (int) (floor(time() / self::BUCKET_SECONDS) * self::BUCKET_SECONDS);
273 }
274
275 private static function incrementCount(array &$counts, string $key): void
276 {
277 if (!isset($counts[$key])) {
278 $counts[$key] = 0;
279 }
280
281 $counts[$key]++;
282 }
283
284 private static function mergeCounts(array &$target, array $source): void
285 {
286 foreach ($source as $key => $count) {
287 if (!isset($target[$key])) {
288 $target[$key] = 0;
289 }
290
291 $target[$key] += (int) $count;
292 }
293 }
294
295 private static function mergeNestedCounts(array &$target, array $source): void
296 {
297 foreach ($source as $groupKey => $counts) {
298 if (!isset($target[$groupKey]) || !is_array($target[$groupKey])) {
299 $target[$groupKey] = [];
300 }
301
302 self::mergeCounts($target[$groupKey], is_array($counts) ? $counts : []);
303 }
304 }
305
306 private static function sortNestedCounts(array $nestedCounts): array
307 {
308 foreach ($nestedCounts as &$counts) {
309 if (is_array($counts)) {
310 arsort($counts);
311 }
312 }
313 unset($counts);
314
315 uksort($nestedCounts, static function ($left, $right) use ($nestedCounts) {
316 $leftTotal = array_sum($nestedCounts[$left] ?? []);
317 $rightTotal = array_sum($nestedCounts[$right] ?? []);
318
319 if ($leftTotal === $rightTotal) {
320 return strcmp((string) $left, (string) $right);
321 }
322
323 return $rightTotal <=> $leftTotal;
324 });
325
326 return $nestedCounts;
327 }
328
329 private static function resolveTrackingSince(array $buckets): ?string
330 {
331 $firstBucket = array_key_first($buckets);
332
333 if ($firstBucket === null) {
334 return null;
335 }
336
337 return gmdate('c', (int) $firstBucket);
338 }
339
340 private static function extractHost(string $url): ?string
341 {
342 if ($url === '') {
343 return null;
344 }
345
346 $host = parse_url($url, PHP_URL_HOST);
347
348 if (!is_string($host) || $host === '') {
349 return null;
350 }
351
352 return strtolower($host);
353 }
354
355 private static function containsAny(string $haystack, array $needles): bool
356 {
357 foreach ($needles as $needle) {
358 if (strpos($haystack, strtolower($needle)) !== false) {
359 return true;
360 }
361 }
362
363 return false;
364 }
365 }
366