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 / LogsWriteRecoveryPolicy.php

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

116 lines 5.0 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 * Handles direct wpdb write recovery for log queue flushes.
9 */
10 class ABJ_404_Solution_LogsWriteRecoveryPolicy {
11
12 /** @var ABJ_404_Solution_Logging */
13 private $logger;
14
15 /** @var ABJ_404_Solution_DatabaseNoticeStateHolder */
16 private $noticeState;
17
18 /**
19 * @param ABJ_404_Solution_Logging $logger
20 * @param ABJ_404_Solution_DatabaseNoticeStateHolder $noticeState
21 */
22 public function __construct(
23 $logger,
24 ABJ_404_Solution_DatabaseNoticeStateHolder $noticeState
25 ) {
26 $this->logger = $logger;
27 $this->noticeState = $noticeState;
28 }
29
30 public function isTableFullError(string $error): bool {
31 $lower = strtolower($error);
32 return stripos($lower, 'is full') !== false || stripos($lower, 'table full') !== false;
33 }
34
35 /**
36 * Free space in logsv2 by deleting the oldest 1000 entries. Rate-limited
37 * to once per hour via a transient cooldown.
38 */
39 public function autoTrimLogsv2IfNeeded(string $tableName, string $errorMessage): bool {
40 if (!preg_match('/^[a-zA-Z0-9_]+$/', $tableName) || strpos($tableName, 'abj404_logsv2') === false) {
41 $this->logger->warn("autoTrimLogsv2IfNeeded: rejected unexpected table name: " . substr($tableName, 0, 100));
42 return false;
43 }
44 $cooldownKey = 'abj404_logsv2_trim_cooldown_until';
45 $alreadyTrimmed = function_exists('get_transient') ? get_transient($cooldownKey) : false;
46 if ($alreadyTrimmed) {
47 return false;
48 }
49 global $wpdb;
50 $trimSql = "DELETE FROM `{$tableName}` ORDER BY timestamp ASC LIMIT 1000";
51 // DAO-bypass-approved: recovery trim must run on the active wpdb handle so last_error reflects the immediate retry context.
52 $wpdb->query($trimSql);
53 $ttl = defined('HOUR_IN_SECONDS') ? (int) HOUR_IN_SECONDS : 3600;
54 // @cache-write-audit: opt-out - log-trim cooldown marker, not query result data.
55 if (function_exists('set_transient')) {
56 set_transient($cooldownKey, 1, $ttl);
57 }
58 if (!empty($wpdb->last_error)) {
59 $this->logger->warn("Log table full: auto-trim failed: " . $wpdb->last_error);
60 } else {
61 $this->logger->warn("Log table full: auto-trimmed 1000 oldest entries to free space.");
62 }
63 return true;
64 }
65
66 public function setLogsv2FullNotice(string $errorMessage): void {
67 $this->noticeState->setPluginDbNotice(
68 'log_table_full',
69 function_exists('__') ? __('The 404 Solution log table is full and cannot accept new entries. This is usually caused by a full disk. Please contact your host or manually prune the logs table.', '404-solution') : 'The 404 Solution log table is full and cannot accept new entries. This is usually caused by a full disk. Please contact your host or manually prune the logs table.',
70 function_exists('__') ? __('The 404 Solution log table is full. The plugin automatically trimmed the oldest 1,000 log entries to free space, but logging may still be limited. Please contact your hosting provider about disk space.', '404-solution') : 'The 404 Solution log table is full. The plugin automatically trimmed the oldest 1,000 log entries to free space, but logging may still be limited. Please contact your hosting provider about disk space.',
71 $errorMessage
72 );
73 }
74
75 public function getWpdbRecentQueryContextForLogs(): string {
76 global $wpdb;
77 if (!isset($wpdb) || !is_object($wpdb)) {
78 return '';
79 }
80 if (!defined('SAVEQUERIES') || SAVEQUERIES !== true) {
81 return '';
82 }
83 if (empty($wpdb->queries) || !is_array($wpdb->queries)) {
84 return '';
85 }
86 $recent = array_slice($wpdb->queries, -5);
87 $parts = [];
88 foreach ($recent as $q) {
89 if (!is_array($q)) {
90 continue;
91 }
92 $sql = $q[0] ?? '';
93 $time = $q[1] ?? null;
94 $caller = $q[2] ?? '';
95 $hash = is_string($sql) ? substr(sha1($sql), 0, 10) : 'n/a';
96 $who = $this->extractWpComponentFromString(is_string($caller) ? $caller : '');
97 $t = is_numeric($time) ? round((float)$time, 3) : 'n/a';
98 $parts[] = "{$who}:{$hash}@{$t}";
99 }
100 return implode(', ', $parts);
101 }
102
103 private function extractWpComponentFromString(string $text): string {
104 $normalized = str_replace('\\', '/', $text);
105 foreach (array('/wp-content/mu-plugins/' => 'mu-plugin', '/wp-content/plugins/' => 'plugin', '/wp-content/themes/' => 'theme') as $needle => $label) {
106 $pos = strpos($normalized, $needle);
107 if ($pos !== false) {
108 $rest = substr($normalized, $pos + strlen($needle));
109 $name = explode('/', ltrim($rest, '/'))[0] ?? '';
110 return $name !== '' ? "{$label}:{$name}" : "{$label}:unknown";
111 }
112 }
113 return 'unknown';
114 }
115 }
116