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:
@@ -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 doesn’t reveal anything specific.
|
* This is a public fatal error that doesn’t 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 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 = $path . 'installed.php';
|
||||||
|
$ts = time() - 21600;
|
||||||
|
if(!is_file($installed) || (filemtime($installed) > $ts)) {
|
||||||
|
// site was installed within the last 6 hours, safe to assume it’s 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
|
||||||
|
Reference in New Issue
Block a user