| @@ -89,9 +89,9 @@ | ||
| 89 | 89 | * Setup WordPress error handlers - Only for plugin-specific errors |
| 90 | 90 | */ |
| 91 | 91 | private function setup_error_handlers() { |
| 92 | 92 | // Store previous handlers to chain them properly |
| 93 | - $this->previous_error_handler = set_error_handler(array($this, 'capture_php_error'), E_ALL); | |
| 93 | + $this->previous_error_handler = set_error_handler(array($this, 'capture_php_error'), E_ALL); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- plugin logger sink / last-resort fallback | |
| 94 | 94 | $this->previous_exception_handler = set_exception_handler(array($this, 'capture_exception')); |
| 95 | 95 | |
| 96 | 96 | // Hook into WordPress fatal error handler (only for plugin errors) |
| 97 | 97 | add_filter('wp_fatal_error_handler', array($this, 'capture_fatal_error')); |
| @@ -123,8 +123,14 @@ | ||
| 123 | 123 | if ($this->previous_error_handler && is_callable($this->previous_error_handler)) { |
| 124 | 124 | $handled = call_user_func($this->previous_error_handler, $severity, $message, $file, $line); |
| 125 | 125 | } |
| 126 | 126 | |
| 127 | + // Respect error_reporting() and the @ suppression operator — never capture masked errors | |
| 128 | + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting, PluginCheck.CodeAnalysis.PHPErrorReporting.DirectErrorReportingCall -- reads the current mask, does not print paths; reading the mask is the only way a custom handler can respect the @ operator | |
| 129 | + if (!(error_reporting() & $severity)) { | |
| 130 | + return $handled; | |
| 131 | + } | |
| 132 | + | |
| 127 | 133 | // Only capture fatal-level errors — skip warnings, notices, deprecated, strict |
| 128 | 134 | $fatal_severities = [E_USER_ERROR, E_RECOVERABLE_ERROR]; |
| 129 | 135 | if (!in_array($severity, $fatal_severities, true)) { |
| 130 | 136 | return $handled; |
| @@ -137,9 +143,9 @@ | ||
| 137 | 143 | 'severity' => $severity, |
| 138 | 144 | 'file' => $file, |
| 139 | 145 | 'line' => $line, |
| 140 | 146 | 'error_level' => $this->get_error_level($severity), |
| 141 | - 'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10) | |
| 147 | + 'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- genuine failure path, bounded, no secrets | |
| 142 | 148 | )); |
| 143 | 149 | } |
| 144 | 150 | |
| 145 | 151 | // Return the result from the previous handler, or false to continue normal error handling |
| @@ -150,10 +156,12 @@ | ||
| 150 | 156 | * Capture uncaught exceptions - Only plugin-specific exceptions |
| 151 | 157 | */ |
| 152 | 158 | public function capture_exception($exception) { |
| 153 | 159 | // First, call the previous exception handler if it exists |
| 160 | + $chained = false; | |
| 154 | 161 | if ($this->previous_exception_handler && is_callable($this->previous_exception_handler)) { |
| 155 | 162 | call_user_func($this->previous_exception_handler, $exception); |
| 163 | + $chained = true; | |
| 156 | 164 | } |
| 157 | 165 | |
| 158 | 166 | // Only capture exceptions from our plugin or directly related to our plugin |
| 159 | 167 | if ($this->should_capture_error($exception->getFile())) { |
| @@ -164,9 +172,16 @@ | ||
| 164 | 172 | 'line' => $exception->getLine(), |
| 165 | 173 | 'trace' => $exception->getTraceAsString(), |
| 166 | 174 | 'exception_object' => $exception |
| 167 | 175 | )); |
| 176 | + return; | |
| 168 | 177 | } |
| 178 | + | |
| 179 | + // Not our exception and nothing else handled it: restore PHP's default | |
| 180 | + // uncaught-exception handling instead of swallowing it silently. | |
| 181 | + if (!$chained) { | |
| 182 | + throw $exception; | |
| 183 | + } | |
| 169 | 184 | } |
| 170 | 185 | |
| 171 | 186 | /** |
| 172 | 187 | * Capture fatal errors |
| @@ -269,9 +284,9 @@ | ||
| 269 | 284 | /** |
| 270 | 285 | * Check if the current environment is localhost/development |
| 271 | 286 | */ |
| 272 | 287 | private function is_localhost() { |
| 273 | - $host = parse_url(home_url(), PHP_URL_HOST); | |
| 288 | + $host = wp_parse_url(home_url(), PHP_URL_HOST); | |
| 274 | 289 | |
| 275 | 290 | // Check for common localhost patterns |
| 276 | 291 | $localhost_patterns = [ |
| 277 | 292 | 'localhost', |
| @@ -284,9 +299,9 @@ | ||
| 284 | 299 | '.localhost' |
| 285 | 300 | ]; |
| 286 | 301 | |
| 287 | 302 | foreach ($localhost_patterns as $pattern) { |
| 288 | - if (strpos($host, $pattern) !== false) { | |
| 303 | + if ($host !== false && $host !== null && strpos($host, $pattern) !== false) { | |
| 289 | 304 | return true; |
| 290 | 305 | } |
| 291 | 306 | } |
| 292 | 307 | |
| @@ -327,9 +342,9 @@ | ||
| 327 | 342 | $context = array_merge($context, array( |
| 328 | 343 | 'wp_url' => home_url(), |
| 329 | 344 | 'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0', |
| 330 | 345 | 'plugin_name' => 'Search Engine Labs SEO (MetaSync)', |
| 331 | - 'error_timestamp' => date('Y-m-d H:i:s'), | |
| 346 | + 'error_timestamp' => gmdate('Y-m-d H:i:s'), | |
| 332 | 347 | 'site_info' => array( |
| 333 | 348 | 'site_title' => get_bloginfo('name'), |
| 334 | 349 | 'wp_version' => get_bloginfo('version'), |
| 335 | 350 | 'admin_email' => get_bloginfo('admin_email'), |