diff --git a/config/config.php b/config/config.php
index bdda1a02..a252a556 100755
--- a/config/config.php
+++ b/config/config.php
@@ -7,6 +7,7 @@ define('RASPI_CONFIG_PROVIDERS', 'config/vpn-providers.json');
define('RASPI_ADMIN_DETAILS', RASPI_CONFIG.'/raspap.auth');
define('RASPI_WIFI_AP_INTERFACE', 'wlan0');
define('RASPI_CACHE_PATH', sys_get_temp_dir() . '/raspap');
+define('RASPI_ERROR_LOG', sys_get_temp_dir() . '/raspap_error.log');
define('RASPI_DEBUG_LOG', 'raspap_debug.log');
define('RASPI_LOG_SIZE_LIMIT', 64);
diff --git a/includes/defaults.php b/includes/defaults.php
index d7b2972e..317b838b 100755
--- a/includes/defaults.php
+++ b/includes/defaults.php
@@ -12,6 +12,7 @@ $defaults = [
'RASPI_ADMIN_DETAILS' => RASPI_CONFIG.'/raspap.auth',
'RASPI_WIFI_AP_INTERFACE' => 'wlan0',
'RASPI_CACHE_PATH' => sys_get_temp_dir() . '/raspap',
+ 'RASPI_ERROR_LOG' => sys_get_temp_dir() . '/raspap_error.log',
'RASPI_DEBUG_LOG' => 'raspap_debug.log',
'RASPI_LOG_SIZE_LIMIT' => 64,
diff --git a/includes/exceptions.php b/includes/exceptions.php
new file mode 100755
index 00000000..c52c04ed
--- /dev/null
+++ b/includes/exceptions.php
@@ -0,0 +1,6 @@
+
+
diff --git a/index.php b/index.php
index 9b755744..1137c3a3 100755
--- a/index.php
+++ b/index.php
@@ -26,6 +26,7 @@
require 'includes/csrf.php';
ensureCSRFSessionToken();
+require_once 'includes/exceptions.php';
require_once 'includes/config.php';
require_once 'includes/autoload.php';
require_once 'includes/defaults.php';
diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo
index 0b0865ab..d0cd5812 100644
Binary files a/locale/en_US/LC_MESSAGES/messages.mo and b/locale/en_US/LC_MESSAGES/messages.mo differ
diff --git a/locale/en_US/LC_MESSAGES/messages.po b/locale/en_US/LC_MESSAGES/messages.po
index 7053ad78..3c403635 100644
--- a/locale/en_US/LC_MESSAGES/messages.po
+++ b/locale/en_US/LC_MESSAGES/messages.po
@@ -1520,3 +1520,11 @@ msgstr "Update complete"
msgid "An error occurred. Check the log at /tmp/raspap_install.log
"
msgstr "An error occurred. Check the log at /tmp/raspap_install.log
"
+#: includes/exceptions.php
+
+msgid "RaspAP Exception"
+msgstr "RaspAP Exception"
+
+msgid "An exception occurred"
+msgstr "An exception occurred"
+
diff --git a/src/RaspAP/Exceptions/ExceptionHandler.php b/src/RaspAP/Exceptions/ExceptionHandler.php
new file mode 100755
index 00000000..d9cf74e9
--- /dev/null
+++ b/src/RaspAP/Exceptions/ExceptionHandler.php
@@ -0,0 +1,69 @@
+
+ * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
+ * @see
+ */
+
+declare(strict_types=1);
+
+namespace RaspAP\Exceptions;
+
+use RaspAP\Exceptions\HtmlErrorRenderer;
+
+class ExceptionHandler
+{
+
+ public function __construct()
+ {
+ $this->setExceptionHandler();
+ $this->setShutdownHandler();
+ }
+
+ public static function handleException($exception)
+ {
+ $errorMessage = (
+ '[' . date('Y-m-d H:i:s') . '] ' .
+ $exception->getMessage() . ' in ' .
+ $exception->getFile() . ' on line ' .
+ $exception->getLine() . PHP_EOL
+ );
+ // Log the exception
+ error_log($errorMessage, 3, RASPI_ERROR_LOG);
+
+ $renderer = new HtmlErrorRenderer();
+ $renderer->render($exception);
+ }
+
+ public static function handleShutdown()
+ {
+ $error = error_get_last();
+ if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
+ $errorMessage = (
+ '[' . date('Y-m-d H:i:s') . '] ' .
+ $error['message'] . ' in ' .
+ $error['file'] . ' on line ' .
+ $error['line'] . PHP_EOL
+ );
+ error_log($errorMessage, 3, RASPI_ERROR_LOG);
+
+ $renderer = new HtmlErrorRenderer();
+ $renderer->render($exception);
+ }
+ }
+
+ protected function setExceptionHandler()
+ {
+ set_exception_handler(array($this, 'handleException'));
+ }
+
+ protected function setShutdownHandler()
+ {
+ register_shutdown_function(array($this, 'handleShutdown'));
+ }
+}
+
diff --git a/src/RaspAP/Exceptions/HtmlErrorRenderer.php b/src/RaspAP/Exceptions/HtmlErrorRenderer.php
new file mode 100755
index 00000000..058a2478
--- /dev/null
+++ b/src/RaspAP/Exceptions/HtmlErrorRenderer.php
@@ -0,0 +1,106 @@
+
+ * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
+ * @see
+ */
+
+declare(strict_types=1);
+
+namespace RaspAP\Exceptions;
+
+class HtmlErrorRenderer
+{
+
+ public function __construct()
+ {
+ $this->charset = 'UTF-8';
+ $this->projectDir = $_SERVER['DOCUMENT_ROOT'];
+ $this->template = '/templates/exception.php';
+ $this->debug = true;
+ }
+
+ public function render($exception)
+ {
+ $message = $exception->getMessage();
+ $code = $exception->getCode();
+ $file = $exception->getFile();
+ $line = $exception->getLine();
+ $trace = $this->getExceptionTraceAsString($exception);
+
+ header('Content-Type: text/html; charset='.$this->charset);
+ if ($this->debug) {
+ header('X-Debug-Exception: '. rawurlencode($message));
+ header('X-Debug-Exception-File: '. rawurlencode($file).':'.$line);
+ }
+ $__template_data = compact(
+ "message",
+ "code",
+ "file",
+ "line",
+ "trace"
+ );
+ if (is_array($__template_data)) {
+ extract($__template_data);
+ }
+ $file = $this->projectDir . $this->template;
+ ob_start();
+ include $file;
+ echo ob_get_clean();
+ }
+
+ /**
+ * Improved exception trace
+ * @param object $e : exception
+ * @param array $seen : passed to recursive calls to accumulate trace lines already seen
+ * @return array of strings, one entry per trace line
+ * @see https://github.com/php/php-src/blob/master/Zend/zend_exceptions.c
+ */
+ public function getExceptionTraceAsString($e, $seen = null) {
+
+ $starter = $seen ? 'Thrown by: ' : '';
+ $result = array();
+ if (!$seen) $seen = array();
+ $trace = $e->getTrace();
+ $prev = $e->getPrevious();
+ $result[] = sprintf('%s%s: %s', $starter, get_class($e), $e->getMessage());
+ $file = $e->getFile();
+ $line = $e->getLine();
+
+ while (true) {
+ $current = "$file:$line";
+ if (is_array($seen) && in_array($current, $seen)) {
+ $result[] = sprintf(' ... %d more', count($trace)+1);
+ break;
+ }
+ $result[] = sprintf(' at %s%s%s(%s%s%s)',
+ count($trace) && array_key_exists('class', $trace[0]) ? str_replace('\\', '.', $trace[0]['class']) : '',
+ count($trace) && array_key_exists('class', $trace[0]) && array_key_exists('function', $trace[0]) ? '.' : '',
+ count($trace) && array_key_exists('function', $trace[0]) ? str_replace('\\', '.', $trace[0]['function']) : '(main)',
+ $line === null ? $file : basename($file),
+ $line === null ? '' : ':',
+ $line === null ? '' : $line);
+
+ if (is_array($seen)) {
+ $seen[] = "$file:$line";
+ }
+ if (!count($trace)) {
+ break;
+ }
+ $file = array_key_exists('file', $trace[0]) ? $trace[0]['file'] : 'Unknown Source';
+ $line = array_key_exists('file', $trace[0]) && array_key_exists('line', $trace[0]) && $trace[0]['line'] ? $trace[0]['line'] : null;
+ array_shift($trace);
+ }
+ $result = join(PHP_EOL, $result);
+ if ($prev) {
+ $result .= PHP_EOL . getExceptionTraceAsString($prev, $seen);
+ }
+ return $result;
+ }
+
+}
+
diff --git a/templates/exception.php b/templates/exception.php
new file mode 100755
index 00000000..5d5c8c49
--- /dev/null
+++ b/templates/exception.php
@@ -0,0 +1,30 @@
+
+
+