PluginProbe
404 Solution / 4.1.19
404 Solution v4.1.19
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 / ajax / AjaxFailureLoggingTrait.php

AjaxFailureLoggingTrait.php in 404 Solution 4.1.19, at includes/ajax/AjaxFailureLoggingTrait.php

130 lines 5.1 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 * AJAX failure-logging utilities used by the AJAX handler classes.
9 *
10 * Four pure static helpers, all callable as `self::method()` from any class
11 * that composes this trait:
12 *
13 * - safeJsonEncode: json_encode wrapper that handles encoding failures so
14 * a malformed payload can never throw inside the failure path itself.
15 * - redactSqlShape: collapse a $wpdb->last_query value into a placeholder
16 * shape (numbers + quoted strings becomes "?") for safe logging.
17 * - safeLogAjaxFailure: write a single error line to the plugin debug log
18 * with summary + details + throwable trace, with a fallback path that
19 * writes next to the plugin file when logging services are unavailable.
20 * - extractViewQueryDiagnostics: walk an exception chain looking for an
21 * ABJ_404_Solution_ViewQueryFailureException and return its diagnostics
22 * payload (table counts, indexes, EXPLAIN, etc.) for the AJAX response.
23 *
24 * Composed into ABJ_404_Solution_ViewUpdater. No state of its own; methods
25 * are static and use only globals (\$GLOBALS['abj404_ajax_context'] is read
26 * by callers, not by these helpers directly) plus the plugin logging service.
27 */
28 trait ABJ_404_Solution_AjaxFailureLoggingTrait {
29
30 /**
31 * @param mixed $value
32 * @return string
33 */
34 private static function safeJsonEncode($value) {
35 $encoded = json_encode($value, JSON_PARTIAL_OUTPUT_ON_ERROR);
36 if ($encoded === false) {
37 return '(json_encode failed) ' . print_r($value, true);
38 }
39 return $encoded;
40 }
41
42 /**
43 * @param mixed $sql
44 * @return string
45 */
46 private static function redactSqlShape($sql) {
47 if (!is_string($sql) || $sql === '') {
48 return '';
49 }
50
51 $out = $sql;
52
53 // Replace quoted strings (single and double quotes) with placeholders.
54 // Note: $wpdb->last_query is a final SQL string and may contain user input values.
55 $out = preg_replace("~'(?:\\\\'|''|[^'])*'~", "?", $out) ?? $out;
56 $out = preg_replace('~"(?:\\\\"|""|[^"])*"~', "?", $out) ?? $out;
57
58 // Replace hex literals and numbers.
59 $out = preg_replace('~\\b0x[0-9A-Fa-f]+\\b~', '?', $out) ?? $out;
60 $out = preg_replace('~\\b\\d+(?:\\.\\d+)?\\b~', '?', $out) ?? $out;
61
62 // Collapse long IN (...) / value lists to a single placeholder.
63 $out = preg_replace('~\\(\\s*\\?\\s*(?:,\\s*\\?\\s*)+\\)~', '(?)', $out) ?? $out;
64 $out = preg_replace('~\\bIN\\s*\\(\\?\\)\\b~i', 'IN (?)', $out) ?? $out;
65
66 // Normalize whitespace and cap length (shape only).
67 $out = preg_replace('~\\s+~', ' ', trim($out)) ?? $out;
68 if (strlen($out) > 4000) {
69 // allow-em-dash: 1-char ellipsis as truncation marker (existing convention)
70 $out = substr($out, 0, 4000) . '';
71 }
72 return $out;
73 }
74
75 /**
76 * @param string $summary
77 * @param mixed $details
78 * @param \Throwable|null $throwable
79 * @return void
80 */
81 private static function safeLogAjaxFailure($summary, $details = null, $throwable = null) {
82 $line = date('c') . ' (ERROR): ' . $summary;
83 if ($details !== null) {
84 $line .= ' Details: ' . self::safeJsonEncode($details);
85 }
86 if ($throwable instanceof Throwable) {
87 $line .= ' Exception: ' . $throwable->getMessage() . ' @ ' . $throwable->getFile() . ':' . $throwable->getLine() .
88 ' Trace: ' . $throwable->getTraceAsString();
89 }
90
91 // Always attempt to write to the plugin debug file.
92 $logger = abj_service('logging');
93 if (is_object($logger) && method_exists($logger, 'writeLineToDebugFile')) {
94 $logger->writeLineToDebugFile($line);
95 return;
96 }
97
98 // Last-resort fallback (should be rare): write next to the plugin.
99 // This ensures we still capture the error even if options/services are broken.
100 if (is_object($logger) && method_exists($logger, 'sanitizeLogLine')) {
101 $line = $logger->sanitizeLogLine($line);
102 }
103 @file_put_contents(ABJ404_PATH . 'abj404_debug_fallback.txt', $line . "\n", FILE_APPEND);
104 }
105
106 /**
107 * If the captured throwable is an ABJ_404_Solution_ViewQueryFailureException
108 * (or a wrapped version of one), return its diagnostics payload. Otherwise
109 * return null. Used by the AJAX error handlers to surface getRedirectsForView /
110 * getRedirectsForViewCount diagnostics (table counts, engine, indexes,
111 * canonical_url state, EXPLAIN, db_version, etc.) to plugin admins and the
112 * debug log without a follow-up debug zip.
113 *
114 * @param Throwable $throwable
115 * @return array<string, mixed>|null
116 */
117 private static function extractViewQueryDiagnostics(Throwable $throwable) {
118 $current = $throwable;
119 $depth = 0;
120 while ($current !== null && $depth < 5) {
121 if ($current instanceof ABJ_404_Solution_ViewQueryFailureException) {
122 return $current->getDiagnostics();
123 }
124 $current = $current->getPrevious();
125 $depth++;
126 }
127 return null;
128 }
129 }
130