<?php
// +-----------------------------------------------------------------------+
// | This file is part of Piwigo.                                          |
// |                                                                       |
// | For copyright and license information, please view the COPYING.txt    |
// | file that was distributed with this source code.                      |
// +-----------------------------------------------------------------------+

/**
 * Modified version of KLogger 0.2.0
 *
 * @author  Kenny Katzgrau <katzgrau@gmail.com>
 *
 * @package logger
 */

class Logger
{
  /**
   * Error severity, from low to high. From BSD syslog RFC, section 4.1.1
   * @link http://www.faqs.org/rfcs/rfc3164.html
   */
  const EMERGENCY = 0;  // Emergency: system is unusable
  const ALERT     = 1;  // Alert: action must be taken immediately
  const CRITICAL  = 2;  // Critical: critical conditions
  const ERROR     = 3;  // Error: error conditions
  const WARNING   = 4;  // Warning: warning conditions
  const NOTICE    = 5;  // Notice: normal but significant condition
  const INFO      = 6;  // Informational: informational messages
  const DEBUG     = 7;  // Debug: debug messages

  /**
   * Custom "disable" level.
   */
  const OFF       = -1; // Log nothing at all

  /**
   * Internal status codes.
   */
  const STATUS_LOG_OPEN  = 1;
  const STATUS_OPEN_FAILED = 2;
  const STATUS_LOG_CLOSED  = 3;

  /**
   * Disable archive purge.
   */
  const ARCHIVE_NO_PURGE = -1;

  /**
   * Standard messages produced by the class.
   * @var array
   */
  private static $_messages = array(
    'writefail'   => 'The file could not be written to. Check that appropriate permissions have been set.',
    'opensuccess' => 'The log file was opened successfully.',
    'openfail'  => 'The file could not be opened. Check permissions.',
  );

  /**
   * Instance options.
   * @var array
   */
  private $options = array(
    'directory' => null, // Log files directory
    'filename' => null, // Path to the log file
    'globPattern' => 'log_*.txt', // Pattern to select all log files with glob()
    'severity' => self::DEBUG, // Current minimum logging threshold
    'dateFormat' => 'Y-m-d G:i:s', // Date format
    'archiveDays' => self::ARCHIVE_NO_PURGE, // Number of files to keep
    );

  /**
   * Current status of the logger.
   * @var integer
   */
  private $_logStatus = self::STATUS_LOG_CLOSED;
  /**
   * File handle for this instance's log file.
   * @var resource
   */
  private $_fileHandle = null;


  /**
   * Class constructor.
   *
   * @param array $options
   * @return void
   */
  public function __construct($options)
  {
    $this->options = array_merge($this->options, $options);
    
    if (is_string($this->options['severity']))
    {
      $this->options['severity'] = self::codeToLevel($this->options['severity']);
    }

    if ($this->options['severity'] === self::OFF)
    {
      return;
    }

    $this->options['directory'] = rtrim($this->options['directory'], '\\/') . DIRECTORY_SEPARATOR;

    if ($this->options['filename'] == null)
    {
      $this->options['filename'] = 'log_' . date('Y-m-d') . '.txt';
    }

    $this->options['filePath'] = $this->options['directory'] . $this->options['filename'];

    if ($this->options['archiveDays'] != self::ARCHIVE_NO_PURGE && rand() % 97 == 0)
    {
      $this->purge();
    }
  }
  
  /**
   * Open the log file if not already oppenned
   */
  private function open()
  {
    if ($this->status() == self::STATUS_LOG_CLOSED)
    {
      if (!file_exists($this->options['directory']))
      {
        mkgetdir($this->options['directory'], MKGETDIR_DEFAULT|MKGETDIR_PROTECT_HTACCESS);
      }

      if (file_exists($this->options['filePath']) && !is_writable($this->options['filePath']))
      {
        $this->_logStatus = self::STATUS_OPEN_FAILED;
        throw new RuntimeException(self::$_messages['writefail']);
        return;
      }

      if (($this->_fileHandle = fopen($this->options['filePath'], 'a')) != false)
      {
        $this->_logStatus = self::STATUS_LOG_OPEN;
      }
      else
      {
        $this->_logStatus = self::STATUS_OPEN_FAILED;
        throw new RuntimeException(self::$_messages['openfail']);
      }
    }
  }

  /**
   * Class destructor.
   */
  public function __destruct()
  {
    if ($this->_fileHandle)
    {
      fclose($this->_fileHandle);
    }
  }

  /**
   * Returns logger status.
   *
   * @return int
   */
  public function status()
  {
    return $this->_logStatus;
  }

  /**
   * Returns logger severity threshold.
   *
   * @return int
   */
  public function severity()
  {
    return $this->options['severity'];
  }

  /**
   * Writes a $line to the log with a severity level of DEBUG.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function debug($line, $cat = null, $args = array())
  {
    $this->log(self::DEBUG, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with a severity level of INFO.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function info($line, $cat = null, $args = array())
  {
    $this->log(self::INFO, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with a severity level of NOTICE.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function notice($line, $cat = null, $args = array())
  {
    $this->log(self::NOTICE, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with a severity level of WARNING.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function warn($line, $cat = null, $args = array())
  {
    $this->log(self::WARNING, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with a severity level of ERROR.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function error($line, $cat = null, $args = array())
  {
    $this->log(self::ERROR, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with a severity level of ALERT.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function alert($line, $cat = null, $args = array())
  {
    $this->log(self::ALERT, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with a severity level of CRITICAL.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function critical($line, $cat = null, $args = array())
  {
    $this->log(self::CRITICAL, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with a severity level of EMERGENCY.
   *
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function emergency($line, $cat = null, $args = array())
  {
    $this->log(self::EMERGENCY, $line, $cat, $args);
  }

  /**
   * Writes a $line to the log with the given severity.
   *
   * @param integer $severity
   * @param string $line
   * @param string $cat
   * @param array $args
   */
  public function log($severity, $message, $cat = null, $args = array())
  {
    if ($this->severity() >= $severity)
    {
      if (is_array($cat))
      {
        $args = $cat;
        $cat = null;
      }
      $line = $this->formatMessage($severity, $message, $cat, $args);
      $this->write($line);
    }
  }

  /**
   * Directly writes a line to the log without adding level and time.
   *
   * @param string $line
   */
  public function write($line)
  {
    $this->open();
    if ($this->status() == self::STATUS_LOG_OPEN)
    {
      if (fwrite($this->_fileHandle, $line) === false)
      {
        throw new RuntimeException(self::$_messages['writefail']);
      }
    }
  }

  /**
   * Purges files matching 'globPattern' older than 'archiveDays'.
   */
  public function purge()
  {
    $files = glob($this->options['directory'] . $this->options['globPattern']);
    $limit = time() - $this->options['archiveDays'] * 86400;

    foreach ($files as $file)
    {
      if (@filemtime($file) < $limit)
      {
        @unlink($file);
      }
    }
  }

  /**
   * Formats the message for logging.
   *
   * @param  string $level
   * @param  string $message
   * @param  array  $context
   * @return string
   */
  private function formatMessage($level, $message, $cat, $context)
  {
    global $page;

    if (!empty($context))
    {
      $message.= "\n" . $this->indent($this->contextToString($context));
    }
    $line = "[" . $this->getTimestamp() . '][exec='.($page['execution_uuid']??'unkonwn')."]\t[" . self::levelToCode($level) . "]\t";
    if ($cat != null)
    {
      $line.= "[" . $cat . "]\t";
    }
    return $line . $message . "\n";
  }

  /**
   * Gets the formatted Date/Time for the log entry.
   *
   * PHP DateTime is dumb, and you have to resort to trickery to get microseconds
   * to work correctly, so here it is.
   *
   * @return string
   */
  private function getTimestamp()
  {
    $originalTime = microtime(true);
    $micro = sprintf('%06d', ($originalTime - floor($originalTime)) * 1000000);
    $date = new DateTime(date('Y-m-d H:i:s.'.$micro, intval($originalTime)));
    return $date->format($this->options['dateFormat']);
  }

  /**
   * Takes the given context and converts it to a string.
   *
   * @param  array $context
   * @return string
   */
  private function contextToString($context)
  {
    $export = '';
    foreach ($context as $key => $value)
    {
      $export.= $key . ': ';
      $export.= preg_replace(array(
        '/=>\s+([a-zA-Z])/im',
        '/array\(\s+\)/im',
        '/^  |\G  /m'
        ),
        array(
        '=> $1',
        'array()',
        '  '
        ),
        str_replace('array (', 'array(', var_export($value, true))
        );
      $export.= PHP_EOL;
    }
    return str_replace(array('\\\\', '\\\''), array('\\', '\''), rtrim($export));
  }

  /**
   * Indents the given string with the given indent.
   *
   * @param  string $string The string to indent
   * @param  string $indent What to use as the indent.
   * @return string
   */
  private function indent($string, $indent = '  ')
  {
    return $indent . str_replace("\n", "\n" . $indent, $string);
  }

  /**
   * Converts level constants to string name.
   *
   * @param int $level
   * @return string
   */
  static function levelToCode($level)
  {
    switch ($level)
    {
      case self::EMERGENCY:
        return 'EMERGENCY';
      case self::ALERT:
        return 'ALERT';
      case self::CRITICAL:
        return 'CRITICAL';
      case self::NOTICE:
        return 'NOTICE';
      case self::INFO:
        return 'INFO';
      case self::WARNING:
        return 'WARNING';
      case self::DEBUG:
        return 'DEBUG';
      case self::ERROR:
        return 'ERROR';
      default:
        throw new RuntimeException('Unknown severity level ' . $level);
    }
  }

  /**
   * Converts level names to constant.
   *
   * @param string $code
   * @return int
   */
  static function codeToLevel($code)
  {
    switch (strtoupper($code))
    {
      case 'EMERGENCY':
        return self::EMERGENCY;
      case 'ALERT':
        return self::ALERT;
      case 'CRITICAL':
        return self::CRITICAL;
      case 'NOTICE':
        return self::NOTICE;
      case 'INFO':
        return self::INFO;
      case 'WARNING':
        return self::WARNING;
      case 'DEBUG':
        return self::DEBUG;
      case 'ERROR':
        return self::ERROR;
      default:
        throw new RuntimeException('Unknown severity code ' . $code);
    }
  }
}
?>