<?php
/*****************************************************************************
 * ErrorHandler.php
 * Contains key class ErrorHandler.
 *
 * Kabuki CMS (C) 2013-2016, Aaron van Geffen
 *****************************************************************************/

class ErrorHandler
{
	private static $error_count = 0;
	private static $handling_error;

	public static function enable()
	{
		set_error_handler('ErrorHandler::handleError');
		ini_set("display_errors", DEBUG ? "On" : "Off");
	}

	public static function disable()
	{
		set_error_handler(NULL);
		ini_set("display_errors", "Off");
	}

	// Handler for standard PHP error messages.
	public static function handleError($error_level, $error_message, $file, $line, $context = null)
	{
		// Don't handle suppressed errors (e.g. through @ operator)
		if (!(error_reporting() & $error_level))
			return;

		// Prevent recursing if we've messed up in this code path.
		if (self::$handling_error)
			return;

		self::$error_count++;
		self::$handling_error = true;

		// Basically, htmlspecialchars it, minus '&' for HTML entities.
		$error_message = strtr($error_message, ['<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "\t" => '  ']);
		$error_message = strtr($error_message, ['&lt;br&gt;' => "<br>",  '&lt;br /&gt;' => "<br>", '&lt;b&gt;' => '<strong>', '&lt;/b&gt;' => '</strong>', '&lt;pre&gt;' => '<pre>', '&lt;/pre&gt;' => '</pre>']);

		// Generate a bunch of useful information to ease debugging later.
		$debug_info = self::getDebugInfo(debug_backtrace());

		// Log the error in the database.
		self::logError($error_message, $debug_info, $file, $line);

		// Are we considering this fatal? Then display and exit.
		// !!! TODO: should we consider warnings fatal?
		if (true) // DEBUG || (!DEBUG && $error_level === E_WARNING || $error_level === E_USER_WARNING))
			self::display($file . ' (' . $line . ')<br>' . $error_message, $debug_info);

		// If it wasn't a fatal error, well...
		self::$handling_error = false;
	}

	public static function getDebugInfo(array $trace)
	{
		$debug_info = "Backtrace:\n";
		$debug_info .= self::formatBacktrace($trace);

		// Include info on the contents of superglobals.
		if (!empty($_SESSION))
			$debug_info .= "\nSESSION: " . var_export($_SESSION, true);
		if (!empty($_POST))
			$debug_info .= "\nPOST: " . var_export($_POST, true);
		if (!empty($_GET))
			$debug_info .= "\nGET: " . var_export($_GET, true);

		return $debug_info;
	}

	private static function formatBacktrace(array $trace)
	{
		$buffer = '';
		$skipping = true;

		foreach ($trace as $i => $call)
		{
			if (isset($call['class']) && ($call['class'] === 'ErrorHandler' || $call['class'] === 'Database') ||
				isset($call['function']) && $call['function'] === 'preg_replace_callback')
			{
				if (!$skipping)
				{
					$buffer .= "[...]\n";
					$skipping = true;
				}
				continue;
			}
			else
				$skipping = false;

			$file = isset($call['file']) ? str_replace(BASEDIR, '', $call['file']) : 'Unknown';
			$object = isset($call['class']) ? $call['class'] . $call['type'] : '';

			$args = [];
			if (isset($call['args']))
			{
				foreach ($call['args'] as $j => $arg)
				{
					// Only include the class name for objects
					if (is_object($arg))
						$args[$j] = get_class($arg) . '{}';
					// Export everything else -- including arrays
					else
						$args[$j] = var_export($arg, true);
				}
			}

			$buffer .= '#' . str_pad($i, 3, ' ')
				. $object . $call['function'] . '(' . implode(', ', $args) . ')'
				. ' called at [' . $file . ':' . $call['line'] . "]\n";
		}

		return $buffer;
	}

	// Logs an error into the database.
	private static function logError($error_message = '', $debug_info = '', $file = '', $line = 0)
	{
		if (!ErrorLog::log([
			'message' => $error_message,
			'debug_info' => $debug_info,
			'file' => str_replace(BASEDIR, '', $file),
			'line' => $line,
			'id_user' => Registry::has('user') ? Registry::get('user')->getUserId() : 0,
			'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
			'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
			]))
		{
			header('HTTP/1.1 503 Service Temporarily Unavailable');
			echo '<h2>An Error Occurred</h2><p>Our software could not connect to the database. We apologise for any inconvenience and ask you to check back later.</p>';
			exit;
		}

		return $error_message;
	}

	public static function display($message, $debug_info, $is_sensitive = true)
	{
		$is_admin = Registry::has('user') && Registry::get('user')->isAdmin();

		// Just show the message if we're running in a console.
		if (empty($_SERVER['HTTP_HOST']))
		{
			echo $message;
			exit;
		}
		// JSON request?
		elseif (isset($_GET['json']) || isset($_GET['format']) && $_GET['format'] == 'json')
		{
			if (DEBUG || $is_admin)
				echo json_encode(['error' => $message . "\n\n" . $debug_info]);
			elseif (!$is_sensitive)
				echo json_encode(['error' => $message]);
			else
				echo json_encode(['error' => 'Our apologies, an error occurred while we were processing your request. Please try again later, or contact us if the problem persists.']);
			exit;
		}

		// Initialise the main template to present a nice message to the user.
		$page = new MainTemplate('An error occurred!');

		// Show the error.
		$is_admin = Registry::has('user') && Registry::get('user')->isAdmin();
		if (DEBUG || $is_admin)
		{
			$page->adopt(new DummyBox('An error occurred!', '<p>' . $message . '</p><pre>' . $debug_info . '</pre>'));

			// Let's provide the admin navigation despite it all!
			if ($is_admin)
			{
				$page->appendStylesheet(BASEURL . '/css/admin.css');
			}
		}
		elseif (!$is_sensitive)
			$page->adopt(new DummyBox('An error occurred!', '<p>' . $message . '</p>'));
		else
			$page->adopt(new DummyBox('An error occurred!', '<p>Our apologies, an error occurred while we were processing your request. Please try again later, or contact us if the problem persists.</p>'));

		// If we got this far, make sure we're not showing stuff twice.
		ob_end_clean();

		// Render the page.
		$page->html_main();
		exit;
	}
}