<?php

/**
 * Classe utilizada para manipular os erros e exceções de forma personalizada
 *
 * @author Marcos Regis <marcos@marcosregis.com>
 * @version 3.0
 * @copyright http://creativecommons.org/licenses/by-sa/3.0/br/
 *
 */
class MyErrorHandler
{

	/**
	 * Endereço de e-mail a qual a mensagem de erro será enviada
	 * @var string
	 */
	private $mail_to;

	/**
	 * Endereço de e-mail do remetente
	 * @var string
	 */
	private $mail_from='no-reply@noreply.com.br';

	/**
	 * Endereço de e-mail para resposta
	 * @var string
	 */
	private $mail_reply_to='no-reply@noreply.com.br';

	/**
	 * Define quais erros deverem ser reportados
	 * NOTA: Os seguintes tipos de erro não podem ser manuseados
	 * por uma função definida pelo usuário: E_ERROR, E_PARSE,
	 * E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR e E_COMPILE_WARNING.
	 * @var array
	 */
	private $user_errors = array(
								E_WARNING,
								// E_NOTICE,
								E_USER_ERROR,
								E_USER_WARNING,
								E_USER_NOTICE,
								// E_STRICT,
								// E_RECOVERABLE_ERROR
							);

	/**
	 * Erros a serem ignorados em ambiente diferente de desenvolvimento
	 * @var array
	 */
	private $ignore_errors = array(E_NOTICE,E_WARNING);

	/** Enviar o erro para um arquivo de log
	 * O segundo parâmetro da função errorlog controla para onde será enviado
	 * a mensagem
	 * '0' A mensagem é enviada para o sistema de log do PHP
	 * usando o sistema de log do sistema operacional ou para um arquivo,
	 * dependendo do que estiver definido na diretiva error_log.
	 * Esta é a opção padrão.
	 *
	 * '1' A mensagem é enviada para o endereço de email no terceiro parâmentro.
	 * Este é o unico tipo de mensagem onde o quarto parâmetro extra_headers é usado.
	 * Este tipo de mensagem usa a mesma função interna que a função mail() usa.
	 *
	 * '2' A mensagem é enviada através de conexão de debug do PHP.
	 * Esta opção só esta disponível se o debug remoto estiver ativado.
	 * Neste caso, o terceiro parâmetro define o nome do servidor ou
	 * endereço IP e opcionalmente, o número da porta, do socket recebendo a
	 * informação de debug.
	 *
	 * '3' A mensagem é adicionada ao arquivo definido no terceiro parâmentro.
	 * @var int
	 */
	private $log_mode=0;

	/**
	 * Arquivo a qual se deseja que o erro seja enviado
	 * @var string
	 */
	private $log_file;

	/**
	 * Url que se deseja redirecionar o fluxo do processamento em caso de erro
	 * evitando as famigeradas telas brancas
	 * @var string
	 */
	private $redirect_to;
	
	/**
	 * Armazena o manipulador existentes antes de se registrar
	 * para que possa ser restaurado em caso de problemas 
	 * @var mixed
	 */
	private $old_handler;
	
	/**
	 * Variável que guarda uma auto instancia (singleton)
	 * @var myErrorHandler
	 *
	static private $instance; */ 

	/**
	 * Construtor da classe
	 * Caso a classe seja configurada diretamente no código não há necessidade 
	 * de chamar o método register através da instancia e portando bastaria 
	 * instanciá-la para que ela se registre como manipulador padrão
	 * O método pode inclusive não ser o construtor e sim um método estático
	 * porém neste caso deverá ser reescrito alguns métodos
	 * @return MyErrorHandler
	 */
	public function MyErrorHandler()
	{
		// Código que deve ser inicializado
		// $this->register();
	}

	
	/**
	 * Registra uma auto-instancia como manipulador de erros padrão do PHP
	 * @return unknown_type
	 */
	public function register()
	{
		$this->old_handler = set_error_handler( array(&$this,'catchMyErrors'));		
	}

	/**
	 * Desregistra a instancia como manipulador de erros retornando ao que era anteriormente
	 * @return MyErrorHandler for Fluent API
	 */
	public function restore()
	{
		set_error_handler($this->old_handler);	
		return $this;		
	}
	/**
	 * Função definida para manipular os erros de execução evitando mostrar os erros
	 * para o usuário e enviando os erros para um tratamento específico
	 * @param Integer $errno Código do erro gerado
	 * @param String $errmsg Mensagem de erro
	 * @param String $filename Nome do arquivo onde o erro foi gerado
	 * @param Integer $linenum Linha do arquivo que gerou o erro
	 * @param mixed $vars Variáveis no momento do erro
	 * @return void
	 */
	public function catchMyErrors($errno, $errmsg, $filename, $linenum, $vars)
	{
		try{
			// Matriz associativa com as strings dos erros
			// Caso não precise da tradução do tipo de erro pode-se
			// usar o código comentado
			$errortype = array (
								E_ERROR => "ERRO FATAL",
								E_WARNING => "ALERTA",
								E_PARSE => "ERRO DE SINTAXE",
								E_NOTICE => "AVISO",
								E_CORE_ERROR => "ERRO DE PROCESSAMENTO",
								E_CORE_WARNING => "ALERTA DE PROCESSAMENTO",
								E_COMPILE_ERROR => "ERRO DE COMPILAÇÃO",
								E_COMPILE_WARNING => "ALERTA DE COMPILAÇÃO",
								E_USER_ERROR => "ERRO DO USUÁRIO",
								E_USER_WARNING => "ALERTA DO USUÁRIO",
								E_USER_NOTICE => "AVISO DO USUÁRIO",
								E_STRICT => "AVISO ESTRITO",
								// E_RECOVERABLE_ERROR => "Erro Recuperável"
							);
			 
			if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1')) && in_array($errno, $this->getIgnoreErrors()))
			{
				return ;
			}
	
			// timestamp para a entrada do erro		
			$dt = date("d-m-Y H:i");
	
			// Corpo da mensagem
			if(function_exists('mb_convert_encoding'))
				// Para evitar problemas com acentos exibidos de forma errada
				$errmsg = $str = mb_convert_encoding($errmsg, "ISO-8859-1", "ASCII,JIS,UTF-8,ISO-8859-1");
			
			$corpomsg = "\r\n<hr noshade />\r\n" .
						"<b>############ Informa&ccedil;&otilde;es sobre o erro: ############</b>\r\n" .
						"<ul>\r\n" .
						"\t<li>Data/Hora : $dt</li>\r\n" .
						"\t<li>Tipo : [$errno] ". htmlentities($errortype[$errno]) ."</li>\r\n" .
						"\t<li>Descri&ccedil;&atilde;o : <small>". htmlentities($errmsg) ."</small></li>\r\n" .
						"\t<li>Arquivo : $filename</li>\r\n" .
						"\t<li>Linha : $linenum</li>\r\n" .
						"</ul>\r\n" .
						"<hr noshade />\r\n";
	
			// Adicionando informações da origem do erro
			if(isset($_SERVER['HTTP_HOST'],$_SERVER['REQUEST_URI'])){
				$corpomsg.="<h3>Outras informa&ccedil;&otilde;es:</h3>\r\n" .
						"<ul>\r\n" .
						"\t<li>IP do usu&aacute;rio: {$_SERVER['REMOTE_ADDR']}</li>\r\n" .
						"\t<li>Url: {$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}</li>\r\n" .
						"</ul>\r\n";
			}
	
			$corpomsg .= "############ ######################### ############";
			/*
			 * Neste momento $corpomsg possui as informações relevantes sobre o erro
			 * e podemos direcionar para algum tratamento
			 * No caso dos portais, para evitar uma visualização poluída ou mesmo
			 * evitar que erros fiquem ocultos nos fontes do HTML podemos imprimir
			 * em forma de alerta.
			 */
			if(in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1')))
			{
				// O erro será exibido na janela em forma de box flutuante
				echo '<div style="display: table; border: 1pt outset #282828;float: left; font: normal 8pt tahoma; color: #8F0000; position: absolute; background-color: #e0e0e0;z-index: 99;" ondblclick="this.parentNode.removeChild(this);">' .
				$corpomsg .
					'</div>';
			}
			else
			{
				// O erro será enviado utilizando a função error_log ou mail
	
				// Redirecionando o fluxo de execução ao encontrar um erro
				// que normalmente paralizaria a execução do script
				if (in_array($errno, $this->user_errors))
				{
						
					// Verificando para onde será enviado o erro
					switch($this->log_mode)
					{
						case 3:
							if($this->log_file!=ini_get('error_log'))
							{
								ini_set('error_log',$this->log_file);
							}
	
						case 1:
							// Tentando enviar por email
							if($this->mail_to!=NULL){
	
								// /* assunto */
								$subject = "Alerta de erro em {$_SERVER['REQUEST_URI']}";
									
								// /* Para enviar email HTML, definido o header Content-type. */
								$headers = "MIME-Version: 1.0\r\n";
								$headers .= "Content-type: text/html; charset=ISO-8859-1\r\n";
									
								// /* headers adicionais */
								$headers .= 'To: '. $this->mail_to ."\r\n";
								$headers .= 'From: '. $this->mail_from ."\r\n";
								$mensagem = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' .
											'<html xmlns="http://www.w3.org/1999/xhtml">' .
											'<head><title>Erro no Site '. $_SERVER['SERVER_NAME'] .'</title>' .
											'<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1" />' .
											'<meta http-equiv="content-language" content="pt-br" />' .
											'<meta name="author" content="Marcos Regis &lt;marcos@marcoregis.com&gt;" />' .
											'<meta name="copyright" content="Marcos Regis - 2007~2010" />' .
											'</head>' .
											'<body>'. $corpomsg .'</body></html>';
								/* Enviar o email com a mensagem de erro*/
								mail($this->mail_to, $this->normalizeSubject($subject), $mensagem, $headers);
									
							}
								
						case 2: // @todo de acordo com o manual não é mais uma opção
	
						default:
							error_log(implode("",array_unique(explode("\n",html_entity_decode(strip_tags($corpomsg))))));
					}
					// se o erro for grave redireciona para uma página definida
					$this->redirect();
				}
			}
		}
		catch (Excpetion $e)
		{
			$this->restore();
		}
	}

	/**
	 * Envia instrução de redirecionamento para a saída
	 */
	protected function redirect()
	{
		if($this->redirect_to!=NULL)
		{
			if(!headers_sent())
			{
				header('Location: ' . $this->redirect_to);
				exit;
			}
			else
			{
				die('<meta http-equiv="refresh" content="1;url='. $this->redirect_to .'" />');
			}
		}
	}
	
	
	/**
	 * Trata uma string convertendo caracteres especiais para seu equivalente
	 * em hexadecimal. Imprescindível para os cabeçalhos de e-mail se necessitar usar caracteres acentuados
	 * @param string $string Texto a ser tratado
	 * @return string Texto tratado
	 */
	protected function charToHex($string) 
	{
		return preg_replace("#([ãáàâäéèêëíìîïõóòôöúùûüçñ])#ie",'"=". dechex(ord("\\1"))',$string);
	}
	
	/**
	 * Converte o texto do assunto para o formato de acordo com o RFC que não me lembro
	 * @param string $subject
	 * @return string
	 */
	protected function normalizeSubject($subject)
	{
		if(preg_match("#([ÃÁÀÂÄÉÈÊËÍÌÎÏÕÓÒÔÖÚÙÛÜÇÑãáàâäéèêëíìîïõóòôöúùûüçñ])#i",$subject))
		{
			$subject =  '=?iso-8859-1?Q?' . $this->charToHex($subject) .'?=';			
		}
		return $subject;
	}
	
	/**
	  * Retorna o valor da propriedade mail_to
	  * @return string mail_to
	  */	
	public function getMailTo(){
		return $this->mail_to;
	}
		
	/**
	  * Set value for property mail_to
	  * @param string {mail_to}
	  * @access public
	  * @return myErrorHandler for Fluent API 
	  */
	public function setMailTo($mail_to) {
		if (preg_match("#^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$#", $mail_to)) // validando o endereço de email
			$this->mail_to=$mail_to;
		return $this;
	}
	
	/**
	  * Retorna o valor da propriedade mail_from
	  * @return string mail_from
	  */	
	public function getMailFrom(){
		return $this->mail_from;
	}
		
	/**
	  * Set value for property mail_from
	  * @param string {mail_from}
	  * @access public
	  * @return myErrorHandler for Fluent API 
	  */
	public function setMailFrom($mail_from) {
		if (preg_match("#^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$#", $mail_from)) // validando o endereço de email
			$this->mail_from=$mail_from;
		return $this;
	}
	
	/**
	  * Retorna o valor da propriedade mail_reply_to
	  * @return string mail_reply_to
	  */	
	public function getMailReplyTo(){
		return $this->mail_reply_to;
	}
		
	/**
	  * Set value for property mail_reply_to
	  * @param string {mail_reply_to}
	  * @access public
	  * @return myErrorHandler for Fluent API 
	  */
	public function setMailReplyTo($mail_reply_to) {
		if (preg_match("#^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$#", $mail_reply_to)) // validando o endereço de email
			$this->mail_reply_to=$mail_reply_to;
		return $this;
	}

	/**
	  * Retorna o valor da propriedade ignore_errors
	  * @return array ignore_errors
	  */	
	public function getIgnoreErrors(){
		return $this->ignore_errors;
	}
		
	/**
	  * Set value for property ignore_errors
	  * @param array {ignore_errors}
	  * @access public
	  * @return myErrorHandler for Fluent API
	  */
	public function setIgnoreErrors(array $ignore_errors) {
		// Somente os existem os tipo de erros abaixo e portanto somente estes poderão ser ignorados
		$errortype = array (
							E_ERROR,
							E_WARNING,
							E_PARSE,
							E_NOTICE,
							E_CORE_ERROR,
							E_CORE_WARNING,
							E_COMPILE_ERROR,
							E_COMPILE_WARNING,
							E_USER_ERROR,
							E_USER_WARNING,
							E_USER_NOTICE,
							E_STRICT,
							// E_RECOVERABLE_ERROR
						);
		$tobeignored=array();
		foreach($ignore_errors as $item)
		{
			if(in_array($item,$errortype))
				$tobeignored[]=$item;
		}
		if(count($tobeignored)>0)
			$this->ignore_errors=$tobeignored;
		
		return $this;
		
	}
	
	/**
	  * Retorna o valor da propriedade user_errors
	  * @return array $user_errors
	  */	
	public function getUserErrors(){
		return $this->user_errors;
	}
		
	/**
	  * Set value for property user_errors
	  * @param array {user_errors}
	  * @access public
	  * @return myErrorHandler for Fluent API
	  */
	public function setUserErrors(array $user_errors) {
		// Somente os existem os tipo de erros abaixo e portanto somente estes poderão ser ignorados
		$errortype = array (
							E_ERROR,
							E_WARNING,
							E_PARSE,
							E_NOTICE,
							E_CORE_ERROR,
							E_CORE_WARNING,
							E_COMPILE_ERROR,
							E_COMPILE_WARNING,
							E_USER_ERROR,
							E_USER_WARNING,
							E_USER_NOTICE,
							E_STRICT,
							// E_RECOVERABLE_ERROR
						);
		$tobemanipulated=array();
		foreach($user_errors as $item)
		{
			if(in_array($item,$errortype))
				$tobemanipulated[]=$item;
		}
		if(count($tobemanipulated)>0)
			$this->user_errors=$tobemanipulated;
		
		return $this;		
	}	
	
	/**
	  * Retorna o valor da propriedade log_mode
	  * @return int log_mode
	  */	
	public function getLogMode(){
		return $this->log_mode;
	}
		
	/**
	  * Se
	  * t value for property log_mode
	  * @param int {log_mode}
	  * @access public
	  * @return myErrorHandler for Fluent API
	  */
	public function setLogMode($log_mode) {
		$this->log_mode=(int)($log_mode);
		return $this;
	}
	
	
	/**
	  * Retorna o valor da propriedade log_file
	  * @return string log_file
	  */	
	public function getLogFile(){
		return ($this->log_file==NULL?ini_get('error_log'):$this->log_file);
	}
		
	/**
	  * Set value for property log_file
	  * Deve ser passado um caminho completo para o arquivo
	  * Será verificado se o PHP tem permissão de escrita neste arquivo
	  * @param string {log_file}
	  * @access public
	  */
	public function setLogFile($log_file) {		
		if(!is_file($log_file)) // verificando se o arquivo já existe, caso contrário tenta criá-lo
		{
			if(is_writable(dirname($log_file))) // PHP tem permissão de escrita no diretório
			{
				if(file_put_contents($log_file,'#########################################\\r\\nArquivo de log criado em '. date('Y-m-d H:i:s'). '\\r\\n#########################################\\r\\n')>0)
				{
					$this->log_file=$log_file;
				}
			}
		}
		else
		{
			if(is_writable($log_file)){
				$this->log_file=$log_file;
			}
		}
		return $this;		
	}
	
	/**
	  * Retorna o valor da propriedade redirect_to
	  * @return string redirect_to
	  */	
	public function getRedirectTo(){
		return $this->redirect_to;
	}
		
	/**
	  * Set value for property redirect_to
	  * O valor deve ser uma url completa ou relativa para uma página de erro
	  * @param string {redirect_to}
	  * @access public
	  */
	public function setRedirectTo($redirect_to) {
		$this->redirect_to=$redirect_to;
	}
}