<?php

use Claromentis\Core\Config\Config;
use Claromentis\Core\Services;

/**
 * Class for correct client error reporting.
 *
 * @obfuscate_disable
 */
class ErrorReporter
{
	static $last_error;
	/**
	 * Enter description here...
	 *
	 * @var Crypt_Xtea
	 */
	protected $crypt;
	protected $error_uniq_id = 0;
	protected $errortype = array (
		E_ERROR           => "Error",
		E_WARNING         => "Warning",
		E_NOTICE          => "Notice",
		E_USER_ERROR      => "User error",
		E_USER_WARNING    => "User warning",
		E_USER_NOTICE     => "User notice",
		E_RECOVERABLE_ERROR => "Recoverable error",
		E_STRICT          => "Runtime Notice",
	);
	/**
	 * @var \Psr\Log\LoggerInterface
	 */
	protected $logger;
	protected $encrypt_errors;
	protected $log_warnings;

	static $error_reporting;

	static public function SetReportingLow()
	{
		$prev_level = error_reporting(E_CLA_COMPAT);

		if (!is_array(self::$error_reporting))
			self::$error_reporting = [$prev_level];
		else
			self::$error_reporting[] = $prev_level;
	}

	static public function SetReportingStandard()
	{
		if (!empty(self::$error_reporting))
			$level = array_pop(self::$error_reporting);
		else
			$level = E_CLA_STANDARD;
		error_reporting($level);
	}

	public function __construct($encrypt_errors, $log_warnings)
	{
		require_once 'Crypt/Xtea.php';
		$this->crypt = new Crypt_Xtea();
		$this->encrypt_errors = $encrypt_errors;
		$this->log_warnings = $log_warnings;
	}

	public function SetLogger($logger)
	{
		$this->logger = $logger;
	}

	/**
	 * Create array with error information
	 *
	 * @param int $errno
	 * @param string $errmsg
	 * @param string $filename
	 * @param int $linenum
	 * @param array $vars
	 * @return array
	 */
	protected function GenerateReportArray($errno, $errmsg, $filename, $linenum, $vars)
	{
		global $g_application;

		if (isset($g_application) && isset($g_application['db.connected']) && $g_application['db.connected'])
		{
			$db = $g_application['db'];

			$version_file = $g_application['core.version_file'];
			$core_version = $version_file->GetVersionName();
		} else
		{
			$db = null;
			$core_version = 'Unknown';
		}

		//prevent XSS attacks
		$errmsg = cla_htmlsafe($errmsg);

		$debug_trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
		array_shift($debug_trace);
		array_shift($debug_trace);

		$error_info = array();
		$err_type_str = (isset($this->errortype[$errno]) ? $this->errortype[$errno] : "Error $errno");
		$error_info['message'] = "<b>$err_type_str:</b> $errmsg in <b>$filename</b> on line <b>$linenum</b><br>";
		$error_info['debug'] = $debug_trace;
		$error_info['db_type'] = is_object($db) ? $db->type() : "No db obj";
		$error_info['REQUEST_URI'] = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';

		$server_software = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '';
		$server_software = substr($server_software, 0, 32);
		$error_info['SERVER_SOFTWARE'] = $server_software;

		$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
		$user_agent = substr($user_agent, 0, 32);
		$error_info['HTTP_USER_AGENT'] = $user_agent;

		$error_info['session_username'] = isset($_SESSION) && isset($_SESSION['SESSION_username']) ? $_SESSION['SESSION_username'] : '';
		$error_info['client_name'] = is_object($db) ? lic_get_name() : '';

		if (strlen($error_info['client_name']) == 0)
			$error_info['client_name'] = 'Unregistered';

		$error_info['cla_version'] = $core_version;

		return $error_info;
	}
	/**
	 * Create encrypted string from error_array
	 *
	 * @param array $error_info
	 * @return string
	 */
	protected function CreateCryptString($error_info)
	{
		$key = 'OBhzRd2D4RxuCjB';
		$encrypted = $this->crypt->encrypt(gzcompress(var_export($error_info, true) ), $key);
		return chunk_split(base64_encode($encrypted), 70, "\r\n");
	}
	/**
	 * Create html for error showing
	 *
	 * @param string $crypt_string
	 * @return string
	 */
	protected function GenerateClientErrText($crypt_string)
	{
		/** @var Config $coreConfig */
		$coreConfig = Services::I()->config;

		$cfg_error_reporting_url = $coreConfig->get('cfg_error_reporting_url');
		$text = <<<EOS
<!doctype html>
<html lang=en>
<head>
	<title>Claromentis - Error Reporter</title>
	<link type="text/css" href="/interface_default/css/bootstrap.min.css" media="screen" rel="stylesheet" />
	<link type="text/css" href="/interface_default/css/glyphicons.min.css" media="all" rel="stylesheet" />
	<style>
	h1 {
		font-size: 24px;
		margin-bottom: 10px;
	}
	h1 .glyphicons {
		font-size: 27px;
		line-height: 16px;
		color: darkred;
	}
	.outer-wrap {
		padding: 0 15px;
	}
	</style>
</head>
<body>
<div class="outer-wrap">
<h1 class="text-error"><span class="glyphicons glyphicons-info-sign"></span> A system error occurred</h1>
<form method="post" action="
EOS;

		$text .= $cfg_error_reporting_url . <<<EOS
">

<div class="card">
	<div class="card-body">
		<p>Please let us know what you were doing, if possible; this will help our dev team fix the problem:</p>
		<p><textarea rows="3" name="error_desc" class="report-textarea form-control" style="height: 60px" id="error_desc"></textarea></p>

		<input value="Send error" class="btn btn-success float-right" type="submit" />

		<div class="clearfix"></div>
		<hr>
		<p>If you don't have a direct internet connection, please include the text from the textarea below when contacting support:</p>
		<textarea wrap="off" rows="6" cols="20" name="crypt_error" onfocus="this.form.crypt_error.select();" class="form-control" style="overflow-x: hidden;" readonly="readonly">
EOS;
		$text .= $crypt_string . <<<EOS
</textarea>

	</div>
</div>
</form>
</div>
<script type="text/javascript">
	document.getElementById('error_desc').focus();
</script>
</body>
</html>
EOS;
		return json_encode($text);
	}
	/**
	 * Create onclick function for displaing error in popup window
	 *
	 * @param string $popup_text
	 * @return string
	 */
	protected function CreateOnClickText($popup_text)
	{
		return cla_htmlsafe("err_win = window.open( '', 'err_win', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=690,height=500, left=300, top=300'); err_win.document.body.innerHTML = ''; err_win.document.write({$popup_text}); return false;");
	}

	/**
	 * Callback for set_error_handler function
	 *
	 * @param int $errno
	 * @param string $errmsg
	 * @param string $filename
	 * @param int $linenum
	 * @param array $vars
	 * @return bool
	 */
	public function ErrorHandler($errno, $errmsg, $filename = "", $linenum = 0, $vars = [])
	{
		self::$last_error = $errmsg;

		$display_this_error = error_reporting() & $errno;

		$log_this_error = $display_this_error ||
			$this->log_warnings && $this->logger && // logging enabled
			($errno & E_CLA_STANDARD) &&
			(error_reporting() !== 0) &&    // do not log errors suppressed with @ operator
			empty(self::$error_reporting);  // do not log errors suppressed with ErrorReporter::SetReportingLow

		if (!$display_this_error && !$log_this_error)
			return false;

		$error_info = $this->GenerateReportArray($errno, $errmsg, $filename, $linenum, $vars);
		$depth = count($error_info['debug']);
		if ($depth > 0)
		{
			$top = $error_info['debug'][$depth-1];
			if ($top['function'] === 'require' && preg_match('|[\\\\/]web[\\\\/]index\.php$|', $top['file']))
				array_pop($error_info['debug']);
		}

		if ($this->logger && $log_this_error)
			$this->logger->error("PHP error: ".$errmsg." at $filename line $linenum", $error_info['debug']);

		if (!$display_this_error)
			return false;

		++$this->error_uniq_id;

		if (!$this->encrypt_errors)
		{
			$error_text = '<br>'.$error_info['message']."(<a href='#' onclick=\"document.getElementById('".$this->error_uniq_id."').style.display = '';return false;\">Show backtrace</a>) <ol id='".$this->error_uniq_id."' style='display:none'>";
			foreach ($error_info['debug'] as $string)
			{
				if (isset($string['class']) && $string['class'] == 'Claromentis\Core\DAL\Db' && isset($string['function']) && $string['function'] == 'SetErrMsg')
					break;
				$error_text .= '<li><b>'.(isset($string['class'])?$string['class']:'').'</b>'.(isset($string['type'])?$string['type']:'').
						'<b>'.(isset($string['function'])?$string['function']:'').'</b>() at ['.(isset($string['file'])?$string['file']:'').' <b>line: '.(isset($string['line'])?$string['line']:'').'</b>]</li>';
			}
			$error_text .= '</ol>';
		} elseif ($errno == E_USER_ERROR)
		{
			$error_text = cla_htmlsafe($errmsg)."<br />";
		} else
		{
			$error_text = "An error has occured ";
		}


		//I don't think this does anything, but I'm leabing it here just in case
		$error_info['message'] = strip_tags($error_info['message']);
		$debug_text = '';
		$xajax_flag = false;
		$count = 0;
		foreach ($error_info['debug'] as $string)
		{
			$count++;
			$debug_text .= "\n".$count.': '.(isset($string['class'])?$string['class']:'').' '.(isset($string['type'])?$string['type']:'').' '.
					(isset($string['function'])?$string['function']:'')."()\n\t ".(isset($string['file'])?$string['file']:'').' at line: ' . (isset($string['line'])?$string['line']:'');

			if (isset($string['class']) && isset($string['function']) && $string['class'].'::'.$string['function'] == 'xajax::processRequests')
				$xajax_flag = true;
		}
		$error_info['debug'] = $debug_text;
		$js_string = $this->GenerateClientErrText( $this->CreateCryptString($error_info) );

		$show_error = $error_text." <a href='#' onclick=\"".$this->CreateOnClickText($js_string)."\">click here</a> for details<br>";
		if (!defined('ERROR_HAS_OCCURED'))
		{
			define('ERROR_HAS_OCCURED', 1);
		}
		if ($xajax_flag)
		{
			echo strip_tags($error_text);
			exit;
		} else
		{
			if (!headers_sent())
			{
				header("Content-type: text/html; charset=utf-8");
				echo "<!doctype html>";
			}

			echo $show_error;
		}
		return true;
	}

	/**
	 * Handler for unhandled exceptions
	 *
	 * @param Exception $exception
	 */
	public function ExceptionHandler($exception)
	{
		/** @var Config $coreConfig */
		$coreConfig = Services::I()->config;

		if ($exception instanceof TokenException)
		{
			//401 or 403 are not correct, 400 is generic "bad request"
			http_response_code(400);
			if (class_exists('AuthUser', false))
			{
				$auth_mode = AuthUser::I()->GetMode();
				if ($auth_mode == AuthUser::MODE_XAJAX || $auth_mode == AuthUser::MODE_AJAX)
				{
					echo "\nToken error\nPOST token is missing or incorrect. This is probably result of session timeout. Please reload the page and try again.\n";
					exit();
				}
			}
			global $args, $template, $message, $is_error_message;
			$is_error_message = 1;
			$message = "Token error";
			$template = "common/token_error.html";
			if (!strcasecmp("GET", $_SERVER["REQUEST_METHOD"]))
			{
				$args["form.visible"] = true;
				$args["form.action"] = $_SERVER['PHP_SELF'];
				$args["inputs.datasrc"] = array();
				foreach ($_GET as $k => $v)
					$args["inputs.datasrc"][] = array("input.name" => $k, "input.value" => $v,);
			}
			$args['link.href'] = $coreConfig->get('cfg_intranet_entry_page');
			require("../common/process_template.php");
			exit;
		} elseif ($exception instanceof \Claromentis\Core\Security\Exception\AuthenticationException)
		{
			switch (AuthUser::I()->GetMode())
			{
				case AuthUser::MODE_AJAX:
				case AuthUser::MODE_POPUP:
				case AuthUser::MODE_XAJAX:
					echo $exception->getMessage();
					exit();
				case AuthUser::MODE_NORMAL:
					httpRedirect("/login?is_error_message=1&message=".urlencode($exception->getMessage()));
					exit();
			}
		} elseif ($exception instanceof \Claromentis\Core\Security\Exception\AdminAccessDeniedException)
		{
			httpRedirect("/intranet/admin/admin.php", lmsg('admin.check_admins.no_rights'), 1);
		}

		//Unhandled exception === the server died trying to serve the request
		http_response_code(500);
		// TODO: This is a horrible way to print uncaught exceptions, improve it
		$this->ErrorHandler(E_ERROR, "Uncaught exception ".get_class($exception).": " . $exception->getMessage() . ', ' . $exception->getTraceAsString(), $exception->getFile(), $exception->getLine(), null);
	}

}
