<?php

/*
 * Configuration :
 * // Log file
 * $log_file='/path/to/app.log';
 *
 * // Log level (DEBUG / INFO / WARNING / ERROR / FATAL)
 * $log_level='INFO';
 *
 * // Log PHP errors levels (as specified to set_error_handler())
 * // $log_php_errors_levels = E_ALL & ~E_STRICT;

 * // Log PHP errors levels (as specified to set_error_handler())
 * // Default:
 * // - In TRACE or DEBUG: E_ALL & ~E_STRICT
 * // - Otherwise: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
 * // $log_php_errors_levels = E_ALL & ~E_STRICT;
 */


// Log file descriptor (Do not change !!!)
$_log_file_fd = null;

// Log Levels
$_log_levels = array(
  'TRACE' => 0,
  'DEBUG' => 1,
  'INFO' => 2,
  'WARNING' => 3,
  'ERROR' => 4,
  'FATAL' => 5,
);

// Custom fatal error handler
$_fatal_error_handler = null;

/**
 * Log a message
 * @param string $level The message level (key of $_log_levels)
 * @param string $message The message to log
 * @return true
 */
function logging($level, $message) {
  global $log_file, $_log_file_fd, $_log_levels, $log_level, $argv,
  $_fatal_error_handler, $auth_user;

  if (!array_key_exists($level, $_log_levels)) $level = 'INFO';
  $level_id = $_log_levels[$level];

  $log_level_id = $_log_levels[$log_level];

  if ($level_id < $log_level_id) return true;
  if(is_null($_log_file_fd)) {
    $_log_file_fd = fopen($log_file, 'a');
  }

  // If more than 2 arguments passed, format message using sprintf
  if (func_num_args() > 2) {
    $message = call_user_func_array(
      'sprintf',
      array_merge(array($message), array_slice(func_get_args(), 2))
    );
  }

  if (php_sapi_name() == "cli") {
    $msg = implode(' - ', array(
      date('Y/m/d H:i:s'),
      basename($argv[0]),
      $level,
      $message
    ))."\n";
  }
  else {
    $msg = array(
      date('Y/m/d H:i:s'),
      $_SERVER['REQUEST_URI'],
      $_SERVER['REMOTE_ADDR'],
    );
    if (isset($auth_user))
      $msg[] = ($auth_user['username']?$auth_user['username']:'anonymous');
    $msg[] = $level;
    $msg[] = $message;
    $msg = implode(' - ', $msg)."\n";
  }

  fwrite($_log_file_fd, $msg);

  if ($level == 'FATAL')
    if (!is_null($_fatal_error_handler))
      call_user_func($_fatal_error_handler, $message);
    elseif (function_exists('fatal_error'))
      fatal_error($message);
    else
      die("\n$message\n\n");
  elseif (php_sapi_name() == "cli")
    echo $msg;

  return true;
}

// Set default log level (if not defined or invalid)
$default_log_level = 'WARNING';
if (!isset($log_level)) {
  $log_level = $default_log_level;
}
elseif (!array_key_exists($log_level, $_log_levels)) {
  $invalid_value = $log_level;
  $log_level = $default_log_level;
  logging(
    $log_level, "Invalid log level value found in configuration (%s). ".
    "Set as default (%s).", $invalid_value, $log_level);
}

/**
 * Register a contextual fatal error handler
 * @param null|callable $handler The fatal error handler (set as null to reset)
 * @return void
 */
function register_fatal_error_handler($handler) {
  // @phpstan-ignore-next-line
  if ($handler && !is_callable($handler))
    logging('FATAL', 'Fatal handler provided is not callable !');
  global $_fatal_error_handler;
  $_fatal_error_handler = ($handler?$handler:null);
}


/**
 * Change of current log file
 * @param string $file The new log file path
 * @return bool
 */
function change_log_file($file) {
  global $log_file, $_log_file_fd;
  if ($file == $log_file) return True;
  if ($_log_file_fd) {
    fclose($_log_file_fd);
    $_log_file_fd = false;
  }
  $log_file = $file;
  return True;
}

/*
 *******************************************************************************
 *            Handle exception logging
 *******************************************************************************
 */

/**
 * Get the current backtrace
 * @param int $ignore_last The number of last levels to ignore
 * @return string
 */
function get_debug_backtrace_context($ignore_last=0) {
  $traces = debug_backtrace();

  // Also ignore this function it self
  $ignore_last++;

  if (!is_array($traces) || count($traces) <= $ignore_last)
    return "";

  $msg = array();
  for ($i=$ignore_last; $i < count($traces); $i++) {
    $trace = array("#$i");
    if (isset($traces[$i]['file']))
      $trace[] = $traces[$i]['file'].(isset($traces[$i]['line'])?":".$traces[$i]['line']:"");
    // @phpstan-ignore-next-line
    if (isset($traces[$i]['class']) && isset($traces[$i]['function']))
      $trace[] = implode(" ", array(
        $traces[$i]['class'],
        $traces[$i]['type'],
        $traces[$i]['function']. "()"));
    elseif (isset($traces[$i]['function']))
      $trace[] = $traces[$i]['function']. "()";
    $msg[] = implode(" - ", $trace);
  }

  return implode("\n", $msg);
}

/**
 * Log an exception
 * @param Throwable $exception
 * @param string|null $prefix The prefix of the log message
 *          (optional, default: "An exception occured")
 * @return void
 */
function log_exception($exception, $prefix=null) {
  if (function_exists('log_in_sentry'))
    log_in_sentry($exception);
  // If more than 2 arguments passed, format prefix message using sprintf
  if ($prefix && func_num_args() > 2) {
    $prefix = call_user_func_array(
      'sprintf',
      array_merge(array($prefix), array_slice(func_get_args(), 2))
    );
  }
  logging(
    "ERROR", "%s:\n%s\n## %s:%d : %s",
    ($prefix?$prefix:"An exception occured"),
    get_debug_backtrace_context(1),
    $exception->getFile(), $exception->getLine(),
    $exception->getMessage());
}
set_exception_handler('log_exception');

/*
 *******************************************************************************
 *            Handle PHP error logging
 *******************************************************************************
 */

/**
 * Convert PHP error number to the corresponding label
 * @param int $errno
 * @return string
 */
function errno2type($errno) {
  $constants = get_defined_constants();
  if (is_array($constants))
    foreach($constants as $label => $value)
      if ($value == $errno && preg_match('/^E_(.*)$/', $label, $m))
        return $m[1];
  return 'UNKNOWN ERROR #'.$errno;
}

/**
 * Log a PHP error
 * Note: method design to be used as callable by set_error_handler()
 * @param int $errno The error number
 * @param string $errstr The error message
 * @param string $errfile The filename that the error was raised in
 * @param int $errline The line number where the error was raised
 * @return false Return false to let the normal error handler continues.
 */
function log_php_error($errno, $errstr, $errfile, $errline) {
  $msg = sprintf(
    "A PHP error occured : [%s] %s\nFile : %s (line : %d)",
    errno2type($errno), $errstr, $errfile, $errline
  );
  logging("ERROR", $msg);
  if (function_exists('log_php_error_in_sentry'))
    log_php_error_in_sentry($errno, $msg);
  return False;
}
if (isset($log_php_errors_levels))
  set_error_handler('log_php_error', $log_php_errors_levels);
elseif (in_array($log_level, array('DEBUG', 'TRACE')))
  set_error_handler('log_php_error', E_ALL & ~E_STRICT);
else
  set_error_handler('log_php_error', E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);

# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab