1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-12 09:44:38 +02:00

Some refactoring and improvements to WireShutdown class

This commit is contained in:
Ryan Cramer
2020-09-11 08:33:09 -04:00
parent 7d71eac1bc
commit f8d9309c27

View File

@@ -3,7 +3,7 @@
/** /**
* ProcessWire shutdown handler * ProcessWire shutdown handler
* *
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer * ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* *
* Look for errors at shutdown and log them, plus echo the error if the page is editable * Look for errors at shutdown and log them, plus echo the error if the page is editable
* *
@@ -36,6 +36,30 @@ class WireShutdown extends Wire {
E_RECOVERABLE_ERROR, E_RECOVERABLE_ERROR,
); );
/**
* Fatal error response info, not used unless set manually by $shutdown->setFatalErrorResponse()
*
* - `code` (int): Fatal error http status code (0=use $config->fatalErrorCode instead)
* - `headers` (array): Any additional headers to include in fatal error, in format [ "Header-Name: Header-Value" ]
* - `adminEmail` (string): Administrator email address to send error to (overrides $config->adminEmail)
* - `fromEmail` (string): From email address for email to administrator (default=same as adminEmail)
* - `emailSubject` (string): Override email subject (default=use built-in translatable subject)
* - `emailBody` (string): Override default email body (text-only). Should have {url}, {user} and {message} placeholders.
* - `words` (array): Spicy but calming words to prepend to visible error messages.
*
* @var array
*
*/
protected $fatalErrorResponse = array(
'code' => 0,
'headers' => array(),
'adminEmail' => '',
'fromEmail' => '',
'emailSubject' => '',
'emailBody' => '',
'words' => array(),
);
/** /**
* Associative array of phrase translations for this module * Associative array of phrase translations for this module
* *
@@ -66,6 +90,12 @@ class WireShutdown extends Wire {
*/ */
const defaultFatalErrorHTML = '<p><b>{message}</b><br /><small>{why}</small></p>'; const defaultFatalErrorHTML = '<p><b>{message}</b><br /><small>{why}</small></p>';
/**
* Default email body for emailed fatal errors
*
*/
const defaultEmailBody = "URL: {url}\nUser: {user}\n\n{message}";
/** /**
* Construct and register shutdown function * Construct and register shutdown function
* *
@@ -79,6 +109,24 @@ class WireShutdown extends Wire {
if(!$config->internal) register_shutdown_function(array($this, 'shutdownExternal')); if(!$config->internal) register_shutdown_function(array($this, 'shutdownExternal'));
} }
/**
* Set fatal error response info including http code, optional extra headers, and more
*
* @param array $options
* - `code` (int): http code to send, or omit to use default (500)
* - `headers` (array): Optional additional headers to send, in format [ "Header-Name: Header-Value" ]
* - `adminEmail` (string): Administrator email address to send error to (overrides $config->adminEmail)
* - `fromEmail` (string): From email address for email to administrator (default=same as adminEmail)
* - `emailSubject` (string): Override email subject (default=use built-in translatable subject)
* - `emailBody` (string): Override default email body (text-only). Should have {url}, {user} and {message} placeholders.
* - `words` (array): Spicy but calming words to prepend to visible error messages.
* @since 3.0.166
*
*/
public function setFatalErrorResponse(array $options) {
$this->fatalErrorResponse = array_merge($this->fatalErrorResponse, $options);
}
/** /**
* Setup our translation labels * Setup our translation labels
* *
@@ -278,7 +326,9 @@ class WireShutdown extends Wire {
*/ */
protected function seasonErrorMessage($message) { protected function seasonErrorMessage($message) {
$spices = array( $spices = $this->fatalErrorResponse['words'];
if(empty($spices)) $spices = array(
'Oops', 'Darn', 'Dangit', 'Oh no', 'Ah snap', 'So sorry', 'Well well', 'Oops', 'Darn', 'Dangit', 'Oh no', 'Ah snap', 'So sorry', 'Well well',
'Ouch', 'Arrgh', 'Umm', 'Snapsicles', 'Oh snizzle', 'Look', 'What the', 'Ouch', 'Arrgh', 'Umm', 'Snapsicles', 'Oh snizzle', 'Look', 'What the',
'Uff da', 'Yikes', 'Aw shucks', 'Oye', 'Rats', 'Hmm', 'Yow', 'Not again', 'Uff da', 'Yikes', 'Aw shucks', 'Oye', 'Rats', 'Hmm', 'Yow', 'Not again',
@@ -286,7 +336,9 @@ class WireShutdown extends Wire {
); );
$spice = $spices[array_rand($spices)]; $spice = $spices[array_rand($spices)];
$message = "{$spice}$message"; if(!ctype_punct(substr($spice, -1))) $spice .= '…';
$message = "$spice $message";
return $message; return $message;
} }
@@ -301,14 +353,22 @@ class WireShutdown extends Wire {
include_once(dirname(__FILE__) . '/WireHttp.php'); include_once(dirname(__FILE__) . '/WireHttp.php');
$http = new WireHttp(); $http = new WireHttp();
$codes = $http->getHttpCodes(); $codes = $http->getHttpCodes();
$code = (int) ($this->config ? $this->config->fatalErrorCode : 500); $code = 500;
if($this->fatalErrorResponse['code']) {;
$code = (int) $this->fatalErrorResponse['code'];
} else if($this->config) {
$code = (int) $this->config->fatalErrorCode;
}
if(!isset($codes[$code])) $code = 500; if(!isset($codes[$code])) $code = 500;
$http->sendStatusHeader($code); $http->sendStatusHeader($code);
foreach($this->fatalErrorResponse['headers'] as $header) {
$http->sendHeader($header);
}
return $code; return $code;
} }
/** /**
* Send a 500 internal server error * Send a fatal error
* *
* This is a public fatal error that doesnt reveal anything specific. * This is a public fatal error that doesnt reveal anything specific.
* *
@@ -419,10 +479,6 @@ class WireShutdown extends Wire {
*/ */
public function shutdown() { public function shutdown() {
/** @var Config|null $config */
/** @var User|null $user */
/** @var Page|null $page */
$error = error_get_last(); $error = error_get_last();
if(!$error) return true; if(!$error) return true;
@@ -431,11 +487,10 @@ class WireShutdown extends Wire {
$this->error = $error; $this->error = $error;
$this->prepareLabels(); $this->prepareLabels();
$config = $this->config; $config = $this->config;
$user = $this->wire('user'); // current user, if present $user = $this->wire()->user; /** @var User|null $user */
$useHTML = isset($_SERVER['HTTP_HOST']); // is this an HTTP request where we can output HTML? $useHTML = isset($_SERVER['HTTP_HOST']); // is this an HTTP request where we can output HTML?
$name = $user ? $user->name : '?'; // user name $name = $user && $user->id ? $user->name : '?'; // user name
$why = ''; // reason why error is being shown, when access allows $who = array(); // who/where the error message has been sent
$who = ''; // who/where the error message has been sent
$message = $this->getErrorMessage($error); $message = $this->getErrorMessage($error);
$url = $this->getCurrentUrl(); $url = $this->getCurrentUrl();
$sendOutput = $config->allowExceptions !== true; $sendOutput = $config->allowExceptions !== true;
@@ -449,49 +504,26 @@ class WireShutdown extends Wire {
if(strlen($ip)) $name = "$name ($ip)"; if(strlen($ip)) $name = "$name ($ip)";
} }
// send error email if applicable // save to errors.txt log file
if($config->adminEmail && $sendOutput && $this->wire('mail')) { if($this->saveFatalLog($url, $name, $message)) {
$n = $this->wire('mail')->new() $who[] = $this->labels['error-logged'];
->to($config->adminEmail)
->from($config->adminEmail)
->subject($this->labels['email-subject'])
->body("Page: $url\nUser: $name\n\n" . str_replace("\t", "\n", $message))
->send();
if($n) $who .= $this->labels['admin-notified'];
}
// save to errors.txt log file if applicable
if($config->paths->logs) {
$log = $this->wire(new FileLog($config->paths->logs . 'errors.txt'));
$log->setDelimeter("\t");
$log->save("$name\t$url\t" . str_replace("\n", " ", $message));
$who .= ($who ? ' ' : '') . $this->labels['error-logged'];
} }
// if not allowed to send output, then do nothing further // if not allowed to send output, then do nothing further
if(!$sendOutput) return true; if(!$sendOutput) return true;
// send error email if applicable
if($this->sendFatalEmail($url, $name, $message)) {
$who[] = $this->labels['admin-notified'];
}
// we populate $why if we're going to show error details for any of the following reasons: // we populate $why if we're going to show error details for any of the following reasons:
// otherwise $why will NOT be populated with anything // otherwise $why will NOT be populated with anything
if($config->debug) { $why = $this->getReasonsWhy();
$why = $this->labels['debug-mode'] . " (\$config->debug = true; => /site/config.php)."; $who = implode(' ', $who);
} else if($config->cli) {
$why = $this->labels['cli-mode'];
} else if($user && $user->isSuperuser()) {
$why = $this->labels['you-superuser'];
} else if($config && is_file($config->paths->root . "install.php")) {
$why = $this->labels['install-php'];
} else if($config && $config->paths->assets && !is_file($config->paths->assets . "active.php")) {
// no login has ever occurred or user hasn't logged in since upgrade before this check was in place
// check the date the site was installed to ensure we're not dealing with an upgrade
$installed = $config->paths->assets . "installed.php";
if(!is_file($installed) || (filemtime($installed) > (time() - 21600))) {
// site was installed within the last 6 hours, safe to assume it's a new install
$why = $this->labels['superuser-never'];
}
}
if($why) { if(count($why)) {
$why = reset($why); // show only 1st reason
$why = $this->labels['shown-because'] . " $why $who"; $why = $this->labels['shown-because'] . " $why $who";
$message = $this->amendErrorMessage($message); $message = $this->amendErrorMessage($message);
$this->sendFatalHeader(); $this->sendFatalHeader();
@@ -502,6 +534,126 @@ class WireShutdown extends Wire {
return true; return true;
} }
/**
* Get reasons why a fatal error message is shown
*
* If error details should not be shown then return a blank array
*
* @return array
*
*/
protected function getReasonsWhy() {
$config = $this->config;
$user = $this->wire()->user;
$why = array();
if($user && $user->isSuperuser()) {
$why[] = $this->labels['you-superuser'];
}
if(!$config) return $why;
if($config->debug) {
$why[] = $this->labels['debug-mode'] . " (\$config->debug = true; => /site/config.php).";
}
if($config->cli) {
$why[] = $this->labels['cli-mode'];
}
if(is_file($config->paths->root . 'install.php')) {
$why[] = $this->labels['install-php'];
}
$path = $config->paths->assets;
if($path && !is_file($path . 'active.php')) {
// no login has ever occurred or user hasnt logged in since upgrade before this check was in place
// check the date the site was installed to ensure we're not dealing with an upgrade
$installed = $path . 'installed.php';
$ts = time() - 21600;
if(!is_file($installed) || (filemtime($installed) > $ts)) {
// site was installed within the last 6 hours, safe to assume its a new install
$why[] = $this->labels['superuser-never'];
}
}
return $why;
}
/**
* Save fatal error to log
*
* @param string $url
* @param string $userName
* @param string $message
* @return bool
*
*/
protected function saveFatalLog($url, $userName, $message) {
// save to errors.txt log file if applicable
$config = $this->config;
if(!$config->paths->logs) return false;
$message = str_replace(array("\n", "\t"), " ", $message);
try {
$log = $this->wire(new FileLog($config->paths->logs . 'errors.txt'));
$log->setDelimeter("\t");
$saved = $log->save("$userName\t$url\t$message");
} catch(\Exception $e) {
$saved = false;
}
return $saved;
}
/**
* Send fatal error email
*
* @param string $url
* @param string $userName
* @param string $message
* @return bool
*
*/
protected function sendFatalEmail($url, $userName, $message) {
$mail = $this->wire()->mail;
if(!$mail) return false;
$adminEmail = $this->fatalErrorResponse['adminEmail'];
if(empty($adminEmail) && $this->config) $adminEmail = $this->config->adminEmail;
if(empty($adminEmail)) return false;
$fromEmail = $this->fatalErrorResponse['fromEmail'];
if(empty($fromEmail)) $fromEmail = $adminEmail;
$emailSubject = $this->fatalErrorResponse['emailSubject'];
if(empty($emailSubject)) $emailSubject = $this->labels['email-subject'];
$emailBody = $this->fatalErrorResponse['emailBody'];
if(empty($emailSubject)) $emailBody = self::defaultEmailBody;
$message = str_replace("\t", "\n", $message);
$emailBody = str_replace(
array('{url}', '{user}', '{message}'),
array($url, $userName, $message),
$emailBody
);
try {
$sent = $mail->to($adminEmail)
->from($fromEmail)
->subject($emailSubject)
->body($emailBody)
->send();
} catch(\Exception $e) {
$sent = false;
}
return $sent ? true : false;
}
/** /**
* Secondary shutdown call when ProcessWire booted externally * Secondary shutdown call when ProcessWire booted externally