From 32f068c5ea0cd87a257c31f8bf4260ff6726da4b Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 21 Feb 2020 11:28:45 -0500 Subject: [PATCH] Minor improvements to WireShutdown fatal error reporting, plus prevent some fatal errors from rendering twice and start converting some Exception only handlers to also accept Throwable (for PHP 7.x \Error or \Exception). --- wire/core/ProcessWire.php | 15 ++- wire/core/WireShutdown.php | 126 +++++++++++++++++--- wire/modules/Process/ProcessPageView.module | 8 +- 3 files changed, 123 insertions(+), 26 deletions(-) diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index 5467164b..de1c2485 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -606,24 +606,27 @@ class ProcessWire extends Wire { * * #pw-internal * - * @param \Exception $e + * @param \Throwable $e Exception or Error * @param string $reason * @param null $page * @param string $url * @since 3.0.142 * */ - public function setStatusFailed(\Exception $e, $reason = '', $page = null, $url = '') { - static $lastException = null; - if($lastException === $e) return; + public function setStatusFailed($e, $reason = '', $page = null, $url = '') { + static $lastThrowable = null; + if($lastThrowable === $e) return; + $isException = $e instanceof \Exception; if(!$page instanceof Page) $page = new NullPage(); $this->setStatus(ProcessWire::statusFailed, array( - 'exception' => $e, + 'throwable' => $e, + 'exception' => $isException ? $e : null, + 'error' => $isException ? null : $e, 'failPage' => $page, 'reason' => $reason, 'url' => $url, )); - $lastException = $e; + $lastThrowable = $e; } /** diff --git a/wire/core/WireShutdown.php b/wire/core/WireShutdown.php index 21fe8dab..0bca0ef3 100644 --- a/wire/core/WireShutdown.php +++ b/wire/core/WireShutdown.php @@ -217,11 +217,16 @@ class WireShutdown extends Wire { */ protected function sendErrorMessage($message, $why, $useHTML) { - $this->sendExistingOutput(); + $hadOutput = $this->sendExistingOutput(); + if($hadOutput) echo "\n\n"; + + if($this->config && $this->config->debug) { + $message = $this->seasonErrorMessage($message); + } // return text-only error if(!$useHTML) { - echo "\n\n$message\n\n$why\n\n"; + echo "$message\n\n$why\n\n"; return; } @@ -236,13 +241,54 @@ class WireShutdown extends Wire { ), $html); // make a prettier looking debug backtrace, when applicable - $html = preg_replace('!(]*>\s*)(#\d+\s+[^<]+)!is', '$1$2', $html); + $style = 'font-family:monospace;font-size:14px'; + $html = preg_replace('!(]*>\s*)(#\d+\s+[^<]+)!is', '$1$2', $html); // reference original file rather than compiled version, when applicable $html = str_replace('assets/cache/FileCompiler/site/', '', $html); + + // remove unnecessary stack trace label + $html = str_replace('Stack trace:<', '<', $html); + + // remove portions of path that are not needed in this output + $rootPath = str_replace('/wire/core/', '/', dirname(__FILE__) . '/'); + $rootPath2 = $this->config ? $this->config->paths->root : ''; + $html = str_replace($rootPath, '/', $html); + if($rootPath2 && $rootPath2 != $rootPath) $html = str_replace($rootPath2, '/', $html); + + // underline filenames + $html = preg_replace('!(\s)/([^\s:(]+?)\.(php|module|inc)!', '$1$2.$3', $html); + // improving spacing between filename and line number (123) + $html = str_replace('(', ' (', $html); + + // ProcessWire namespace is assumed so does not need to add luggage to output + $html = str_replace('ProcessWire\\', '', $html); + // output the error message - echo "\n\n$html\n\n"; + echo "$html\n\n"; + } + + /** + * Provide additional seasoning for error message during debug mode output + * + * @param string $message + * @return string + * + */ + protected function seasonErrorMessage($message) { + + $spices = array( + 'Oops', 'Darn', 'Dangit', 'Oh no', 'Ah snap', 'So sorry', 'Well well', + 'Ouch', 'Arrgh', 'Umm', 'Snapsicles', 'Oh snizzle', 'Look', 'What the', + 'Uff da', 'Yikes', 'Aw shucks', 'Oye', 'Rats', 'Hmm', 'Yow', 'Not again', + 'Look out', 'Hey now', 'Breaking news', 'Excuse me', + ); + + $spice = $spices[array_rand($spices)]; + $message = "{$spice}… $message"; + + return $message; } /** @@ -254,13 +300,21 @@ class WireShutdown extends Wire { * @param bool $useHTML Output for a web browser? * */ - protected function sendError500($message, $useHTML) { + protected function sendFatalError($message, $useHTML) { + include_once(dirname(__FILE__) . '/WireHttp.php'); + $http = new WireHttp(); + $codes = $http->getHttpCodes(); + $code = (int) $this->config ? $this->config->fatalErrorCode : 500; + if(!isset($codes[$code])) $code = 500; + if($useHTML) { - header("HTTP/1.1 500 Internal Server Error"); + header("HTTP/1.1 $code " . $codes[$code]); $message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); // file that error message will be output in, when available - $file = $this->config->paths->templates . 'errors/500.html'; + $path = $this->config->paths->templates; + $file = $path . "errors/$code.html"; + if(!file_exists($file) && $code !== 500) $file = $path . "errors/500.html"; } else { $file = ''; } @@ -279,33 +333,72 @@ class WireShutdown extends Wire { /** * Send any existing output while removing PHP’s error message from it (to avoid duplication) * + * @return bool Returns true if there was existing output, false if not + * */ protected function sendExistingOutput() { - + + /* $files = TemplateFile::getRenderStack(); - if(!count($files)) return; + if(!count($files)) { + // existing output (if present) is not from a template file being rendered + return false; + } + */ - $out = ob_get_clean(); - if(!strlen($out)) return; + $out = ob_get_level() ? ob_get_clean() : ''; + if(!strlen(trim($out))) return false; - // if error message isn't in existing output, then reutrn as-is - if(empty($this->error['message']) || strpos($out, $this->error['message']) === false) { + // if error message isn't in existing output, then return as-is + if(empty($this->error['message'])) { echo $out; - return; + return true; } + // encode message the same way that PHP does by default + $message = htmlspecialchars($this->error['message'], ENT_COMPAT | ENT_HTML401, ini_get('default_charset'), false); + + if(strpos($out, $message) !== false) { + // encoded message present in output + } else if(strpos($out, $this->error['message']) !== false) { + // non-encoded message present in output + $message = $this->error['message']; + } else { + // error message not present in output + echo $out; + return true; + } + + // generate a unique token placeholder for message $token = ''; do { $token .= 'xPW' . mt_rand() . 'SD'; } while(strpos($out, $token) !== false); // replace error message with token - $out = str_replace($this->error['message'], $token, $out); + $out = str_replace($message, $token, $out); // replace anything else on the same line as the PHP error (error type, file, line-number) $out = preg_replace('/([\r\n]|^)[^\r\n]+' . $token . '[^\r\n]*/', '', $out); + + // ensure certain tags that could interfere with error message output are closed + $tags = array( + '
' => '
', + '
 '
', + '' => '
', + ' '
', + ); + foreach($tags as $openTag => $closeTag) { + $openPos = strripos($out, $openTag); + if($openPos === false) continue; + $closePos = strripos($out, $closeTag); + if($closePos && $closePos > $openPos) continue; + $out .= $closeTag; + } echo $out; + + return $out === $token ? false : true; } /** @@ -393,7 +486,7 @@ class WireShutdown extends Wire { $message = $this->amendErrorMessage($message); $this->sendErrorMessage($message, $why, $useHTML); } else { - $this->sendError500($who, $useHTML); + $this->sendFatalError($who, $useHTML); } return true; @@ -405,6 +498,7 @@ class WireShutdown extends Wire { */ public function shutdownExternal() { if(error_get_last()) return; + /** @var ProcessPageView $process */ $process = $this->wire('process'); if($process == 'ProcessPageView') $process->finished(); } diff --git a/wire/modules/Process/ProcessPageView.module b/wire/modules/Process/ProcessPageView.module index f3496157..b685ec78 100644 --- a/wire/modules/Process/ProcessPageView.module +++ b/wire/modules/Process/ProcessPageView.module @@ -272,17 +272,17 @@ class ProcessPageView extends Process { } /** - * Hook called when the pageview failed to finish due to an exception. + * Hook called when the pageview failed to finish due to an Exception or Error. * - * Sends a copy of the exception that occurred. + * Sends a copy of the throwable that occurred. * - * @param \Exception $e + * @param \Throwable $e Exception or Error * @param string $reason * @param Page|null $page * @param string $url * */ - public function ___failed(\Exception $e, $reason = '', $page = null, $url = '') { + public function ___failed($e, $reason = '', $page = null, $url = '') { $this->wire()->setStatusFailed($e, $reason, $page, $url); }