PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / logs / LogsRequestedUrlColumnMetadata.php

LogsRequestedUrlColumnMetadata.php in 404 Solution trunk, at includes/logs/LogsRequestedUrlColumnMetadata.php

133 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Resolves and caches metadata for logsv2.requested_url so log writes can
9 * adapt to legacy charsets and engine-specific collation reporting.
10 */
11 class ABJ_404_Solution_LogsRequestedUrlColumnMetadata {
12
13 /** @var ABJ_404_Solution_Logging */
14 private $logger;
15
16 /** @param ABJ_404_Solution_Logging $logger */
17 public function __construct($logger) {
18 $this->logger = $logger;
19 }
20
21 /**
22 * Look up (and cache for a week) the character set and collation of the
23 * logsv2.requested_url column.
24 *
25 * @param string $logTableName
26 * @param mixed $wpdb
27 * @return array{charset_name: ?string, collation_name: ?string}|null
28 */
29 public function resolveRequestedUrlColumnMeta(string $logTableName, $wpdb): ?array {
30 static $cached = null;
31 if ($cached !== null) {
32 return $cached;
33 }
34
35 $cachedMeta = $this->readCachedColumnMeta();
36 if ($cachedMeta !== null) {
37 $cached = $cachedMeta;
38 return $cached;
39 }
40
41 $dbName = defined('DB_NAME') ? (string)DB_NAME : '';
42 if ($dbName === '' || !is_object($wpdb)
43 || !method_exists($wpdb, 'prepare') || !method_exists($wpdb, 'get_results')) {
44 return null;
45 }
46
47 $meta = $this->queryRequestedUrlColumnMeta($logTableName, $dbName, $wpdb);
48 if ($meta === null) {
49 return null;
50 }
51 $this->cacheColumnMeta($meta);
52 $cached = $meta;
53 return $meta;
54 }
55
56 /**
57 * @return array{charset_name: ?string, collation_name: ?string}|null
58 */
59 private function readCachedColumnMeta(): ?array {
60 if (!function_exists('get_transient')) {
61 return null;
62 }
63 $existing = get_transient('abj404_logs_requested_url_column_meta');
64 if (is_array($existing)) {
65 return array(
66 'charset_name' => is_string($existing['charset_name'] ?? null) ? $existing['charset_name'] : null,
67 'collation_name' => is_string($existing['collation_name'] ?? null) ? $existing['collation_name'] : null,
68 );
69 }
70 $legacyCharset = get_transient('abj404_logs_requested_url_charset');
71 if (is_string($legacyCharset) && $legacyCharset !== '') {
72 return array('charset_name' => $legacyCharset, 'collation_name' => null);
73 }
74 return null;
75 }
76
77 /**
78 * @return array{charset_name: ?string, collation_name: ?string}|null
79 */
80 private function queryRequestedUrlColumnMeta(string $logTableName, string $dbName, object $wpdb): ?array {
81 if (!method_exists($wpdb, 'prepare') || !method_exists($wpdb, 'get_results')) {
82 return null;
83 }
84 // DAO-bypass-approved: metadata probe needs wpdb prepare/get_results against information_schema and reads no plugin data.
85 $getCharsetQuery = $wpdb->prepare("SELECT character_set_name as charset_name, collation_name as collation_name \n FROM information_schema.columns \n WHERE lower(table_schema) = lower(%s) \n AND lower(table_name) = lower(%s) \n AND lower(column_name) = lower(%s) ", $dbName, $logTableName, 'requested_url');
86 // DAO-bypass-approved: paired information_schema metadata read for requested_url charset/collation probe.
87 $resultArray = $wpdb->get_results($getCharsetQuery, ARRAY_A);
88 if (empty($resultArray)) {
89 return null;
90 }
91 $firstRow = is_array($resultArray[0]) ? $resultArray[0] : array();
92 $row = array_change_key_case($firstRow, CASE_LOWER);
93 $meta = array(
94 'charset_name' => is_string($row['charset_name'] ?? null) ? $row['charset_name'] : null,
95 'collation_name' => is_string($row['collation_name'] ?? null) ? $row['collation_name'] : null,
96 );
97 return $meta;
98 }
99
100 /**
101 * @param array{charset_name: ?string, collation_name: ?string} $meta
102 */
103 private function cacheColumnMeta(array $meta): void {
104 if (!function_exists('set_transient')) {
105 return;
106 }
107 // @cache-write-audit: opt-out - schema metadata probe cache, not user-query result data.
108 // allow-cache-empty: $meta is built from a non-empty $resultArray in queryRequestedUrlColumnMeta(), so charset_name / collation_name are populated DB metadata, not a failed-fetch sentinel.
109 $ttl = defined('WEEK_IN_SECONDS') ? WEEK_IN_SECONDS : 604800;
110 set_transient('abj404_logs_requested_url_column_meta', $meta, $ttl);
111 if (!empty($meta['charset_name'])) {
112 set_transient('abj404_logs_requested_url_charset', $meta['charset_name'], $ttl);
113 }
114 }
115
116 /** Emit a one-shot warning the first time a non-utf8 logs charset is observed. */
117 public function warnLogsCharsetMismatchOnce(string $logTableName, string $charset): void {
118 if (!function_exists('get_transient') || !function_exists('set_transient')) {
119 return;
120 }
121 $warnKey = 'abj404_warned_logs_charset_mismatch';
122 $warnVal = $logTableName . '|' . strtolower($charset);
123 $already = get_transient($warnKey);
124 if ($already === $warnVal) {
125 return;
126 }
127 $ttl = defined('WEEK_IN_SECONDS') ? WEEK_IN_SECONDS : 604800;
128 // @cache-write-audit: opt-out - charset warning dedup marker, not query result data.
129 set_transient($warnKey, $warnVal, $ttl);
130 $this->logger->warn("Logs table column charset is '{$charset}' for {$logTableName}. URL-encoding stored requested URLs to avoid charset issues.");
131 }
132 }
133