1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-12 01:34:31 +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 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
*
@@ -36,6 +36,30 @@ class WireShutdown extends Wire {
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
*
@@ -66,6 +90,12 @@ class WireShutdown extends Wire {
*/
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
*
@@ -79,6 +109,24 @@ class WireShutdown extends Wire {
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
*
@@ -278,7 +326,9 @@ class WireShutdown extends Wire {
*/
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',
'Ouch', 'Arrgh', 'Umm', 'Snapsicles', 'Oh snizzle', 'Look', 'What the',
'Uff da', 'Yikes', 'Aw shucks', 'Oye', 'Rats', 'Hmm', 'Yow', 'Not again',
@@ -286,7 +336,9 @@ class WireShutdown extends Wire {
);
$spice = $spices[array_rand($spices)];
$message = "{$spice}$message";
if(!ctype_punct(substr($spice, -1))) $spice .= '…';
$message = "$spice $message";
return $message;
}
@@ -301,14 +353,22 @@ class WireShutdown extends Wire {
include_once(dirname(__FILE__) . '/WireHttp.php');
$http = new WireHttp();
$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;
$http->sendStatusHeader($code);
foreach($this->fatalErrorResponse['headers'] as $header) {
$http->sendHeader($header);
}
return $code;
}
/**
* Send a 500 internal server error
* Send a fatal error
*
* This is a public fatal error that doesnt reveal anything specific.
*
@@ -419,10 +479,6 @@ class WireShutdown extends Wire {
*/
public function shutdown() {
/** @var Config|null $config */
/** @var User|null $user */
/** @var Page|null $page */
$error = error_get_last();
if(!$error) return true;
@@ -431,11 +487,10 @@ class WireShutdown extends Wire {
$this->error = $error;
$this->prepareLabels();
$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?
$name = $user ? $user->name : '?'; // user name
$why = ''; // reason why error is being shown, when access allows
$who = ''; // who/where the error message has been sent
$name = $user && $user->id ? $user->name : '?'; // user name
$who = array(); // who/where the error message has been sent
$message = $this->getErrorMessage($error);
$url = $this->getCurrentUrl();
$sendOutput = $config->allowExceptions !== true;
@@ -449,49 +504,26 @@ class WireShutdown extends Wire {
if(strlen($ip)) $name = "$name ($ip)";
}
// send error email if applicable
if($config->adminEmail && $sendOutput && $this->wire('mail')) {
$n = $this->wire('mail')->new()
->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'];
// save to errors.txt log file
if($this->saveFatalLog($url, $name, $message)) {
$who[] = $this->labels['error-logged'];
}
// if not allowed to send output, then do nothing further
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:
// otherwise $why will NOT be populated with anything
if($config->debug) {
$why = $this->labels['debug-mode'] . " (\$config->debug = true; => /site/config.php).";
} 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'];
}
}
$why = $this->getReasonsWhy();
$who = implode(' ', $who);
if($why) {
if(count($why)) {
$why = reset($why); // show only 1st reason
$why = $this->labels['shown-because'] . " $why $who";
$message = $this->amendErrorMessage($message);
$this->sendFatalHeader();
@@ -502,6 +534,126 @@ class WireShutdown extends Wire {
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