| 1 |
<?php |
| 2 |
/** |
| 3 |
* AllAccessible — centralized debug-mode helper. |
| 4 |
*/ |
| 5 |
|
| 6 |
if (!defined('ABSPATH')) { exit; } |
| 7 |
|
| 8 |
final class AllAccessible_Debug { |
| 9 |
|
| 10 |
/** |
| 11 |
* Cache the toggle for the duration of a single request |
| 12 |
* |
| 13 |
* @var bool|null |
| 14 |
*/ |
| 15 |
private static $cached_enabled = null; |
| 16 |
|
| 17 |
/** |
| 18 |
* Is debug logging enabled? Reads aacb_options.debug_mode once per |
| 19 |
* request and caches the value. |
| 20 |
*/ |
| 21 |
public static function is_enabled(): bool { |
| 22 |
if (self::$cached_enabled === null) { |
| 23 |
$opts = get_option('aacb_options', array()); |
| 24 |
self::$cached_enabled = !empty($opts['debug_mode']); |
| 25 |
} |
| 26 |
return self::$cached_enabled; |
| 27 |
} |
| 28 |
|
| 29 |
/** |
| 30 |
* Emit a console.groupCollapsed payload to the browser DevTools. |
| 31 |
* No-op when debug_mode is off. |
| 32 |
* |
| 33 |
* @param string $label Surface name shown in the console group title. |
| 34 |
* Example: "EditorMetaBox — post 42". |
| 35 |
* @param mixed $payload Anything JSON-encodable. Usually an |
| 36 |
* associative array of {requested, response, |
| 37 |
* render_branch, ...}. |
| 38 |
* @param array $opts Optional: |
| 39 |
* - 'group_method' => 'group'|'groupCollapsed' |
| 40 |
* (default 'groupCollapsed' to keep noise low) |
| 41 |
* - 'console_method' => 'log'|'table' |
| 42 |
* (default 'log'; use 'table' for row-shaped |
| 43 |
* arrays where columns help readability) |
| 44 |
*/ |
| 45 |
public static function console(string $label, $payload, array $opts = array()): void { |
| 46 |
if (!self::is_enabled()) { |
| 47 |
return; |
| 48 |
} |
| 49 |
|
| 50 |
$group_method = isset($opts['group_method']) ? (string) $opts['group_method'] : 'groupCollapsed'; |
| 51 |
$console_method = isset($opts['console_method']) ? (string) $opts['console_method'] : 'log'; |
| 52 |
|
| 53 |
// Whitelist to avoid injecting arbitrary method names into the |
| 54 |
// emitted JS. Anything not on this list falls back to safe defaults. |
| 55 |
$allowed_group = array('group', 'groupCollapsed'); |
| 56 |
$allowed_console = array('log', 'table', 'info', 'warn'); |
| 57 |
if (!in_array($group_method, $allowed_group, true)) $group_method = 'groupCollapsed'; |
| 58 |
if (!in_array($console_method, $allowed_console, true)) $console_method = 'log'; |
| 59 |
|
| 60 |
$title_safe = '[AllAccessible] ' . $label; |
| 61 |
$payload_json = wp_json_encode($payload); |
| 62 |
|
| 63 |
// Stable formatting so console output diff-reads cleanly between |
| 64 |
// pageloads when debugging. Title goes through wp_json_encode for |
| 65 |
// proper JS string escaping (handles quotes, backslashes, etc). |
| 66 |
$title_json = wp_json_encode($title_safe); |
| 67 |
|
| 68 |
echo '<script>(function(){console.' . esc_html($group_method) . '(' . $title_json . ');console.' . esc_html($console_method) . '(' . $payload_json . ');console.groupEnd();})();</script>'; |
| 69 |
} |
| 70 |
|
| 71 |
/** |
| 72 |
* Convenience — dump common API-response context in one call. |
| 73 |
* Standardizes the shape across plugin surfaces so debug output is |
| 74 |
* grep-friendly in the console. |
| 75 |
*/ |
| 76 |
public static function api(string $surface, $request, $response): void { |
| 77 |
if (!self::is_enabled()) { |
| 78 |
return; |
| 79 |
} |
| 80 |
self::console($surface, array( |
| 81 |
'request' => $request, |
| 82 |
'is_wp_error' => is_wp_error($response), |
| 83 |
'error' => is_wp_error($response) ? $response->get_error_message() : null, |
| 84 |
'response' => is_wp_error($response) ? null : $response, |
| 85 |
)); |
| 86 |
} |
| 87 |
|
| 88 |
/** |
| 89 |
* Hard failure — something that should never happen and we want to |
| 90 |
* see aggregated across the installed base. Sent to error reporting. |
| 91 |
* |
| 92 |
* @param string $surface Where it happened (e.g. |
| 93 |
* "PostLinkBackfill::link_single"). |
| 94 |
* @param string|\Throwable|\WP_Error $err Message string, exception, or WP_Error. |
| 95 |
* @param array $context Extra fields (post_id, batch_size, etc). |
| 96 |
*/ |
| 97 |
public static function error(string $surface, $err, array $context = array()): void { |
| 98 |
$message = self::error_to_string($err); |
| 99 |
$payload = array_merge(array('surface' => $surface), $context); |
| 100 |
|
| 101 |
// 1. error_log — always. |
| 102 |
error_log(sprintf('[AllAccessible] %s ERROR: %s', $surface, $message)); |
| 103 |
|
| 104 |
// 2. Browser console — only when debug toggle on. Reuses the |
| 105 |
// existing console pipeline so the user sees the same |
| 106 |
// structured payload they'd see for normal debug dumps. |
| 107 |
if (self::is_enabled()) { |
| 108 |
self::console($surface . ' [error]', array_merge( |
| 109 |
array('message' => $message), |
| 110 |
$payload |
| 111 |
), array('console_method' => 'warn')); |
| 112 |
} |
| 113 |
|
| 114 |
// 3. Error reporting. Throwables go to capture_exception (preserves |
| 115 |
// stack trace); strings + WP_Errors go to capture_message. |
| 116 |
if (class_exists('AllAccessible_Sentry')) { |
| 117 |
if ($err instanceof \Throwable) { |
| 118 |
AllAccessible_Sentry::capture_exception($err, $payload); |
| 119 |
} else { |
| 120 |
AllAccessible_Sentry::capture_message( |
| 121 |
sprintf('[%s] %s', $surface, $message), |
| 122 |
'error', |
| 123 |
$payload |
| 124 |
); |
| 125 |
} |
| 126 |
} |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Soft failure — something recoverable but worth noting. Error |
| 131 |
* reporting still captures (severity=warning) so we can see rate patterns. |
| 132 |
*/ |
| 133 |
public static function warn(string $surface, string $message, array $context = array()): void { |
| 134 |
$payload = array_merge(array('surface' => $surface), $context); |
| 135 |
|
| 136 |
error_log(sprintf('[AllAccessible] %s WARN: %s', $surface, $message)); |
| 137 |
|
| 138 |
if (self::is_enabled()) { |
| 139 |
self::console($surface . ' [warn]', array_merge( |
| 140 |
array('message' => $message), |
| 141 |
$payload |
| 142 |
), array('console_method' => 'warn')); |
| 143 |
} |
| 144 |
|
| 145 |
if (class_exists('AllAccessible_Sentry')) { |
| 146 |
AllAccessible_Sentry::capture_message( |
| 147 |
sprintf('[%s] %s', $surface, $message), |
| 148 |
'warning', |
| 149 |
$payload |
| 150 |
); |
| 151 |
} |
| 152 |
} |
| 153 |
|
| 154 |
/** |
| 155 |
* Informational — cron skips, cooldown hits, eligibility gates. |
| 156 |
* NEVER goes to error reporting (would flood it). error_log + |
| 157 |
* console only. Use this instead of bare error_log so a future |
| 158 |
* "route all noisy info elsewhere" decision is one-file. |
| 159 |
*/ |
| 160 |
public static function info(string $surface, string $message, array $context = array()): void { |
| 161 |
$payload = array_merge(array('surface' => $surface), $context); |
| 162 |
|
| 163 |
error_log(sprintf('[AllAccessible] %s INFO: %s', $surface, $message)); |
| 164 |
|
| 165 |
if (self::is_enabled()) { |
| 166 |
self::console($surface . ' [info]', array_merge( |
| 167 |
array('message' => $message), |
| 168 |
$payload |
| 169 |
)); |
| 170 |
} |
| 171 |
} |
| 172 |
|
| 173 |
/** |
| 174 |
* Normalize anything throwable/wp-erroric/stringy into a single |
| 175 |
* human-readable line. |
| 176 |
*/ |
| 177 |
private static function error_to_string($err): string { |
| 178 |
if (is_string($err)) { |
| 179 |
return $err; |
| 180 |
} |
| 181 |
if ($err instanceof \Throwable) { |
| 182 |
return sprintf('%s: %s', get_class($err), $err->getMessage()); |
| 183 |
} |
| 184 |
if (is_wp_error($err)) { |
| 185 |
return sprintf('WP_Error[%s]: %s', $err->get_error_code(), $err->get_error_message()); |
| 186 |
} |
| 187 |
return wp_json_encode($err); |
| 188 |
} |
| 189 |
} |
| 190 |
|