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:
@@ -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 doesn’t 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 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
|
||||
|
Reference in New Issue
Block a user