| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Normalizes queued log entries into the scalar storage shape accepted by |
| 9 |
* logsv2 INSERTs, and serializes frontend pipeline traces for storage. |
| 10 |
*/ |
| 11 |
class ABJ_404_Solution_LogsEntrySanitizer { |
| 12 |
|
| 13 |
/** |
| 14 |
* Normalize a log entry into the strict shape expected by INSERT IGNORE. |
| 15 |
* Returns null when a required column is missing or sanitization rejects |
| 16 |
* the entry (empty requested_url, empty dest_url, or non-scalar payload). |
| 17 |
* |
| 18 |
* @param array<string, mixed> $entry |
| 19 |
* @return array<string, mixed>|null |
| 20 |
*/ |
| 21 |
public function sanitizeLogEntry(array $entry): ?array { |
| 22 |
$required = array('timestamp', 'user_ip', 'referrer', 'dest_url', 'requested_url', 'requested_url_detail', 'username', 'min_log_id', 'engine'); |
| 23 |
foreach ($required as $key) { |
| 24 |
if (!array_key_exists($key, $entry)) { |
| 25 |
return null; |
| 26 |
} |
| 27 |
} |
| 28 |
|
| 29 |
$normalizeString = function($value, $maxLen) { |
| 30 |
if (is_object($value) || is_array($value)) { |
| 31 |
return null; |
| 32 |
} |
| 33 |
$str = (string)$value; |
| 34 |
if (function_exists('mb_convert_encoding')) { |
| 35 |
$converted = mb_convert_encoding($str, 'UTF-8', 'UTF-8'); |
| 36 |
if (is_string($converted)) { |
| 37 |
$str = $converted; |
| 38 |
} |
| 39 |
} |
| 40 |
return substr($str, 0, $maxLen); |
| 41 |
}; |
| 42 |
|
| 43 |
$sanitized = array(); |
| 44 |
$tsVal = $entry['timestamp'] ?? abj_clock()->now(); |
| 45 |
$sanitized['timestamp'] = absint(is_scalar($tsVal) ? $tsVal : abj_clock()->now()); |
| 46 |
$sanitized['user_ip'] = $normalizeString($entry['user_ip'], 512); |
| 47 |
$sanitized['referrer'] = $normalizeString($entry['referrer'], 512); |
| 48 |
$sanitized['dest_url'] = $normalizeString($entry['dest_url'], 512); |
| 49 |
$sanitized['requested_url'] = $normalizeString($entry['requested_url'], 2048); |
| 50 |
$sanitized['requested_url_detail'] = $normalizeString($entry['requested_url_detail'], 2048); |
| 51 |
if (!is_string($sanitized['requested_url']) || $sanitized['requested_url'] === '' |
| 52 |
|| !is_string($sanitized['dest_url']) || $sanitized['dest_url'] === '') { |
| 53 |
return null; |
| 54 |
} |
| 55 |
|
| 56 |
if (array_key_exists('canonical_url', $entry) && is_string($entry['canonical_url'])) { |
| 57 |
$canonical = $entry['canonical_url']; |
| 58 |
} else { |
| 59 |
$canonical = '/' . trim($sanitized['requested_url'], '/'); |
| 60 |
} |
| 61 |
$sanitized['canonical_url'] = substr($canonical, 0, 2048); |
| 62 |
|
| 63 |
$usernameVal = $entry['username'] ?? null; |
| 64 |
$sanitized['username'] = ($usernameVal === null || !is_scalar($usernameVal)) ? null : absint($usernameVal); |
| 65 |
$minLogIdVal = $entry['min_log_id'] ?? null; |
| 66 |
$sanitized['min_log_id'] = ($minLogIdVal === null || !is_scalar($minLogIdVal)) ? null : absint($minLogIdVal); |
| 67 |
$sanitized['engine'] = $normalizeString($entry['engine'], 64); |
| 68 |
if (array_key_exists('pipeline_trace', $entry)) { |
| 69 |
$traceVal = $entry['pipeline_trace']; |
| 70 |
$sanitized['pipeline_trace'] = ($traceVal === null || is_string($traceVal)) ? $traceVal : null; |
| 71 |
} else { |
| 72 |
$sanitized['pipeline_trace'] = null; |
| 73 |
} |
| 74 |
return $sanitized; |
| 75 |
} |
| 76 |
|
| 77 |
/** |
| 78 |
* gz + base64 the pipeline trace payload for storage. |
| 79 |
* |
| 80 |
* @param array<int, array{step: string, outcome: string, detail: string}>|null $trace |
| 81 |
*/ |
| 82 |
public function serializePipelineTrace(?array $trace): ?string { |
| 83 |
if ($trace === null || empty($trace)) { |
| 84 |
return null; |
| 85 |
} |
| 86 |
$json = json_encode($trace); |
| 87 |
if ($json === false) { |
| 88 |
return null; |
| 89 |
} |
| 90 |
$compressed = gzcompress($json, 6); |
| 91 |
if ($compressed === false) { |
| 92 |
return null; |
| 93 |
} |
| 94 |
return base64_encode($compressed); |
| 95 |
} |
| 96 |
} |
| 97 |
|