diff --git a/wire/core/WireShutdown.php b/wire/core/WireShutdown.php
index 9e643de4..fbb8842b 100644
--- a/wire/core/WireShutdown.php
+++ b/wire/core/WireShutdown.php
@@ -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 = '
{message}
{why}
';
+ /**
+ * 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