<?php
// prevenir invocacion directa del script
defined('PRONTUS') OR exit('');
/**
 * Logger
 *
 * Clase Estatica con metodos para manejar y centralizar el log de errores.
 * La idea es mantener centralizado el acceso al los log.
 *
 * Sinopsis:
 * <code>
 * Logger::$level = 'debug'; // debug + errores
 * Logger::$modulo = 'articulo'; // --> articulo.log
 * Logger::mensajeLog('Ingresa al foreach', true); // true --> con dump de variables
 * </code>
 *
 * @package     _core
 * @subpackage  altavoz
 * @author      CVI/YCC <desarrollo@altavoz.net>
 * @version
 * 1.0.0 - 18/11/2008 - CVI - Primera version<br>
 * 1.1.0 - 27/11/2008 - YCC - Transforma en objeto dinamico, agrega metodos varDumper(), customErrorHandler()
 * y variables $level, $dirLog, $modulo.
 * 1.1.1 - 27/11/2008 - YCC - Amplia tamano por defecto de archivos log, de 4Kb a 32 Kb
 * 1.1.2 - 24/09/2009 - JBJ - Se cambia formato de nombre de log historico, ahora la extension de los archivos es .log
 * 1.1.3 - 27/12/2010 - CVI - Rotacion de logs ejecutada al momento de alcanzar el tamanio limite.
 * 1.2.0 - 06/10/2011 - GPB/JOR - Agrega soporte para deprecated introducido en PHP 5.3.x para soportar bibliiotecas que aun no implementen (Ej: PEAR Mail 1.2.0) =>
 *                      [Assigning the return value of new by reference is deprecated]
 */
class Logger {

    /**
     * Nivel de info enviada a log de errores.
     * Valores posibles: debug | error | nolog (default=debug)
     * 'debug': imprime mensajes generados por errorSistema(), errorSistemaExit() y mensajes de
     * debug generados por mensajeLog()
     * 'error': imprime solo los mensajes generados por errorSistema() y errorSistemaExit()
     * 'nolog': no imprime nada
     */
    private $level;

    /**
     * Nombre del directorio donde se almacenan los logs
     * Por omision es cpan/_applog o frontend/_applog o ws/_applog
     * dependiendo de donde se este posicionado.
     */
    private $dirLog;

    /**
     * Nombre del archivo que se usara para el log
     * Usualmente sera el nombre del modulo en que se este trabajando.
     * Por omision, se asume 'global' --> global.log
     * Ejemplo:
     * Logger::$modulo = 'articulo'; // --> articulo.log
     */
    private $modulo;

    /**
     * Full Path al archivo de log
     */
    private $pathLog;

    /**
     * Tamano maximo del archivo de log, una vez excedido el arch. se flushea
     */
    private $maxSizeLog;

    /**
     * Indica si se registraron en el log los mensajes tipo E_STRICT
     */
    private $checkStrict;

    private $notifMsg = true;

    private $exitOnError = true;

    public $copy2Stderr = false;

    // 1.2.0
    public $allow_e_deprecated = false;

    /**
     * Constructor de la clase.
     * Se pueden setear algunos valores de configuracion.
     * @param string $inModulo Nombre del modulo. Default: 'global'
     * @param string $inLevel Nivel de "verbosidad" del Log. Default: 'debug'
     * @param int $inMaxSizeLog Tamanio maximo del Log en bytes. Default: 32768
     * @param int $inDirLog Lugar donde se guardaran los logs. Default: $_SERVER['DOCUMENT_ROOT'] . dirname($_SERVER['PHP_SELF']) . '/_applog'
     */
    public function __construct($inModulo = 'global', $inLevel = 'debug', $inMaxSizeLog = '', $inDirLog = '') {

        $this->checkStrict = false;
        if ($inMaxSizeLog !== '' && is_int($inMaxSizeLog)) {
            $this->maxSizeLog = $inMaxSizeLog;
        } else {
            $this->maxSizeLog = 65535;
        }
        $this->level = $inLevel;
        $this->modulo = $inModulo;
        if (empty($inDirLog)) {
            $inDirLog = $_SERVER['DOCUMENT_ROOT'] . dirname($_SERVER['PHP_SELF']) . '/_applog';
        }
        $this->dirLog = $inDirLog;
        $this->pathLog = $this->dirLog . '/' . $this->modulo . '.log';

        $msgErr = $this->validLog(); // valida archivo y dir de log
        if ($msgErr) {
            error_log($msgErr);
            exit();
        }
        set_error_handler(array($this, 'customErrorHandler'));

    }

    /**
     * Destructor de la clase.
     */
    public function __destruct() {

        restore_error_handler();
    }

    /**
     * Accesor para cambiar el valor Maximo de bytes del Log, antes de rotar
     * @param string $bytesValue Tamanio maximo del Log en bytes
     */
    public function setMaxSizeLog($bytesValue) {

        if (is_int($bytesValue)) $this->maxSizeLog = $bytesValue;
    }


    /**
     * Permite setear parametro que indica si el programa continua despues de un error
     * @param boolean $value Si es true programa continua, si es false programa se detiene
     */
    public function setExitOnError($value) {

        $this->exitOnError = $value;
    }

    /**
     * Permite setear parametro que indica si notificar del mensaje al log principal
     * @param boolean $value
     */
    public function setNotifMsg($value) {

        $this->notifMsg = $value;
    }

    /**
     * Escribe un error de sistema en el log de errores
     * @param string  $msg Mensaje a escribir
     * @param boolean $varDump (opcional, default=false) Indica si se desea agregar o no un vaciado
     * de variables en memoria al mensaje.
     */
    public function errorSistema($msg, $varDump = false) {

        if ($this->level == 'error' || $this->level == 'debug') {
            if ($varDump) $msg .= "\nDump de variables:\n" . $this->varDumper();
            $this->toLog('!![ERROR]' . $msg);
        }
    }

    /**
     * Escribe un error de sistema en el log de errores y hace un stop critico
     * @param string $msg Mensaje a escribir
     * @param boolean $varDump (opcional, default=false) Indica si se desea agregar o no un vaciado
     * de variables en memoria al mensaje.
     */
    public function errorSistemaExit($msg, $varDump = false) {

        if ($this->level=='error' || $this->level=='debug') {
            if ($varDump) $msg .= "\nDump de variables:\n" . $this->varDumper();
            $this->toLog('!![ERROR_EXIT] ' . $msg);
        }
        exit();
    }

    /**
     * Escribe un mensaje en el log de errores
     * @param string $msg Mensaje a escribir
     * @param boolean $varDump (opcional, default=false) Indica si se desea agregar o no un vaciado
     * de variables en memoria al mensaje.
     */
    public function mensajeLog($msg, $varDump = false) {

        if ($this->level == 'debug') {
            if ($varDump) $msg .= "\nDump de variables:\n" . $this->varDumper();
            $this->toLog('[MSG] ' . $msg);
        }
    }

    /**
     * Escribe un mensaje en el log de errores DEPRECATED!!!!
     * @param string $msg Mensaje a escribir
     * @param boolean $varDump (opcional, default=false) Indica si se desea agregar o no un vaciado
     * de variables en memoria al mensaje.
     */
    public static function staticMensajeLog($modulo, $msg,$notifMsg=true) {
        $thisLogger = new Logger($modulo);
        $thisLogger->setNotifMsg($notifMsg);
        $thisLogger->mensajeLog($msg);
    }

    /**
     * Agrega a la salida, que imprimen las funciones publicas, un vaciado de las
     * variables en memoria.
     * @return string conteniendo el dump de variables.
     */
    private function varDumper() {
        ob_start();
        var_dump(debug_backtrace());
        $varDumpContent = ob_get_contents();
        ob_end_clean();
        return $varDumpContent;
    }

    /**
     * Chequea los directorios donde se escribira el Log
     * @return string Si es '', no hubo error. En caso contrario, se devuelve el error.
     */
    private function validLog() {
        // Chequea dir
        $dir = $this->dirLog;
        if (!is_dir($dir)) {
            try {
                mkdir($dir);
            } catch (Exception $e) {
                return "Logger.class: No se pudo crear el directorio de log [$dir] (" . $e->getMessage() . ')';
            }
        }

        // Chequea file
        $result = error_log('', 3, $this->pathLog);
        if (!$result) {
            return "Logger.class: No se pudo escribir en archivo de Log [$pathLog] del directorio [$dirLog]";
        }
        return '';
    }

    /**
     * toLog()
     * Funcion encargada de escribir en el log de errores, lo que los metodos
     * anteriores necesiten escribir. Asi se centraliza en este punto.
     * Actualmente se escribe en un archivo de texto las operaciones y
     * mensajes no criticos.
     * Asume que el dir de log y el nombre del archivo ya fueron validados por el constructor.
     *
     * @param string $msg Mensaje a escribir
     * @return void
     */
    private function toLog($msg) {

        $dir = $this->dirLog;
        $newPathLog='';
        $dt = date('Y-m-d H:i:s (T)');

        if (is_file($this->pathLog)) {
            if (filesize($this->pathLog) >= $this->maxSizeLog) {

                // Respalda el actual
                $newPathLog = dirname($this->pathLog).'/'.basename($this->pathLog, '.log').'_'.date('YmdHis').'.log';
                //$newPathLog = $this->pathLog . '.' . date('YmdHis');
                rename($this->pathLog, $newPathLog);

                // rotar los antiguos
                error_log('Rotando logs...');
                $this->garbageFiles();

            }
        }

        $result = error_log("[$dt] $msg\n", 3, $this->pathLog);
        if ($result && $this->notifMsg) {

            if ($this->copy2Stderr) { // feature util para crons (apps no monitoreadas)
                error_log('Error below was added to: ' . $this->pathLog);
                error_log("[$dt] $msg");
            } else {
                //~ error_log('VerLog: ' . $this->pathLog);
            }
        }
    }


    /**
     * Manejo customizado del Error.
     * Este metodo se llama, cuando se produce un error en el codigo.
     * Asi se puede definir el log en donde deseo escribir el error, en vez de usar el error
     * log por defecto del servidor, entre otras cosas.
     *
     * @param $errno
     * @param $errstr
     * @param $errfile
     * @param $errline
     * @return true
     */
    public function customErrorHandler($errno, $errstr, $errfile, $errline) {

        $error='';
        // Significado de constantes de error: http://cl.php.net/manual/es/errorfunc.constants.php
        $error_type = array(
                            1     =>'E_ERROR',
                            2     =>'E_WARNING',
                            4     =>'E_PARSE',
                            8     =>'E_NOTICE',
                            16     =>'E_CORE_ERROR',
                            32     =>'E_CORE_WARNING',
                            64     =>'E_COMPILE_ERROR',
                            128     =>'E_COMPILE_WARNING',
                            256     =>'E_USER_ERROR',
                            512     =>'E_USER_WARNING',
                            1024     =>'E_USER_NOTICE',
                            6143     =>'E_ALL',
                            2048     =>'E_STRICT',
                            4096     =>'E_RECOVERABLE_ERROR'
                            );
        $this->notifMsg = true; // por si acaso fue desactivada por fuera. Fuerza que el error sxe notifica al error.log ppal
        if ($this->exitOnError) {

            // Termina ejecucion en caso de error
            // 1.2.0
            if ($this->allow_e_deprecated) {
                if ($errno != 8192) { // E_DEPRECATED.
                    if (array_key_exists($errno, $error_type)) {
                        $error = '(' . $error_type[$errno] . "): [$errstr]\nLine [$errline] in file [$errfile]";
                        if ($errno == 2048) { // E_STRICT
                            if ($this->checkStrict) $this->toLog($error);

                        } else {
                            $this->errorSistemaExit($error);
                        }
                    } else {
                        $this->errorSistemaExit("(PHP Unknown error type): [$errstr]\nLine [$errline] in file [$errfile]");
                    }
                }
            } else {
                if (array_key_exists($errno, $error_type)) {
                    $error = '(' . $error_type[$errno] . "): [$errstr]\nLine [$errline] in file [$errfile]";
                    if ($errno == 2048) { // E_STRICT
                        if ($this->checkStrict) $this->toLog($error);

                    } else {
                        $this->errorSistemaExit($error);
                    }
                } else {
                    $this->errorSistemaExit("(PHP Unknown error type): [$errstr]\nLine [$errline] in file [$errfile]");
                }
            }
        } else {
            // Se imprime al log y continua
            if (array_key_exists($errno, $error_type)) {
                $error = '(' . $error_type[$errno] . "): [$errstr]\nLine [$errline] in file [$errfile]";
            } else {
                $error = "(PHP Unknown error type): [$errstr]\nLine [$errline] in file [$errfile]";
            }
            $this->errorSistema($error);
        }


        /* Don't execute PHP internal error handler */
        return true;
    }

    /**
     * garbageFiles()
     *
     * Esta funcion es llamada desde la aplicacion solo cuando se rota un log por tamanio max.
     * Se descartan archivos de log para dejar
     *
     * @return void
     */
    public function garbageFiles() {

        $mod = $this->modulo;
        $dir = $this->dirLog;
        $files = array();
        $counter = 0;
        if (is_dir($dir)) {
            $contenido = scandir($dir); // orden default es alfabetico ascendente
            foreach ($contenido as $idx => $archivo) {
                if ($archivo != '.' && $archivo != '..') {
                    if (preg_match('/'.$mod.'_([0-9]{14})\.log$/', $archivo, $matches)) {
                        $counter++;
                        if ($counter > 20) { // recorre los archivos y cuando la cuenta sobrepasa el max de archivos, empieza a borrar.
                            $res = unlink($dir.'/'.$archivo);
                            if ($res === false) {
                                error_log('Error borrando el archivo... ' . $dir.'/'.$archivo);
                            }
                        }
                    }
                }
            }
        }
    }
}

/*
Notas sobre set_error_handler():

Es importante recordar que el gestor de errores estandar de PHP es completamente ignorado.
Los parametros de error_reporting() no tendran efecto alguno y su gestor de errores sera llamado en
cualquier circunstancia - sin embargo es posible leer el valor actual de error_reporting y actuar
apropiadamente. Es particularmente importante notar que este valor sera 0 si la sentencia que causo
el error fue precedida por el operador de control de errores @.

Note tambien que es su responsabilidad usar die() si es necesario. Si la funcion de gestion de
errores retorna, la ejecucion del script continuara con la siguiente sentencia despues de aquella
que causo el error.

Los siguientes tipos de error no pueden ser gestionados con una funcion definida por el usuario:
E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, y la mayoria
de E_STRICT generados en el archivo en donde set_error_handler() es llamado.

Si ocurren errores antes de que el script es ejecutado (p.ej. cuando se cargan archivos desde una
pagina web) el gestor de errores personalizado no puede ser llamado ya que no esta registrado en
ese momento.
*/
