<?php

use EesyPHP\Log;
use EesyPHP\SentrySpan;
use EesyPHP\SentryTransaction;

use function EesyPHP\format_callable;
use function EesyPHP\vardump;

/*
 * Configured URL patterns :
 *
 *  Example :
 *
 *   array (
 *     '|get/(?P<name>[a-zA-Z0-9]+)$|' => array (
 *       'handler' => 'get',
 *       'authenticated' => true,
 *       'api_mode' => false,
 *       'methods' => array('GET'),
 *     ),
 *     '|get/all$|' => => array (
 *       'handler' => 'get_all',
 *       'authenticated' => true,
 *       'api_mode' => false,
 *       'methods' => array('GET', 'POST'),
 *     ),
 *   )
 *
 */
$url_patterns =array();

/**
 * Add an URL pattern
 *
 * @param string|array      $pattern          The URL pattern or an array of patterns (required)
 * @param callable          $handler          The URL pattern handler (must be callable, required)
 * @param boolean           $authenticated    Permit to define if this URL is accessible only for
 *                                            authenticated users (optional, default: true if the
 *                                            special force_authentication function is defined,
 *                                            false otherwise)
 * @param boolean           $override         Allow override if a command already exists with the
 *                                            same name (optional, default: false)
 * @param boolean           $api_mode         Enable API mode (optional, default: false)
 * @param array|string|null $methods          HTTP method (optional, default: array('GET', 'POST'))
 **/
function add_url_handler($pattern, $handler=null, $authenticated=null, $override=true,
                         $api_mode=false, $methods=null) {
  $authenticated = (
    is_null($authenticated)?
    function_exists('force_authentication'):
    (bool)$authenticated
  );
  if (is_null($methods))
    $methods = array('GET', 'POST');
  elseif (!is_array($methods))
    $methods = array($methods);
  global $url_patterns;
  if (is_array($pattern)) {
    if (is_null($handler))
      foreach($pattern as $p => $h)
        add_url_handler($p, $h, $authenticated, $override, $api_mode, $methods);
    else
      foreach($pattern as $p)
        add_url_handler($p, $handler, $authenticated, $override, $api_mode, $methods);
  }
  else {
    if (!isset($url_patterns[$pattern])) {
      $url_patterns[$pattern] = array(
        'handler' => $handler,
        'authenticated' => $authenticated,
        'api_mode' => $api_mode,
        'methods' => $methods,
      );
    }
    elseif ($override) {
      Log :: debug(
        "URL : override pattern '%s' with handler '%s' (old handler = '%s')".
        $pattern, format_callable($handler), vardump($url_patterns[$pattern])
      );
      $url_patterns[$pattern] = array(
        'handler' => $handler,
        'authenticated' => $authenticated,
        'api_mode' => $api_mode,
        'methods' => $methods,
      );
    }
    else {
      Log :: debug("URL : pattern '$pattern' already defined : do not override.");
    }
  }
}

/**
 * Show error page
 *
 * @param $request UrlRequest|null The request (optional, default: null)
 * @param $error_code int|null The HTTP error code (optional, default: 400)
 *
 * @return void
 **/
function error_page($request=null, $error_code=null) {
  global $smarty;
  $http_errors = array(
    400 => array(
      'pagetitle' => _("Bad request"),
      'message' => _("Invalid request."),
    ),
    401 => array(
      'pagetitle' => _("Authentication required"),
      'message' => _("You have to be authenticated to access to this page."),
    ),
    403 => array(
      'pagetitle' => _("Access denied"),
      'message' => _("You do not have access to this application. If you think this is an error, please contact support."),
    ),
    404 => array(
      'pagetitle' => _("Whoops ! Page not found"),
      'message' => _("The requested page can not be found."),
    ),
  );
  $error_code = ($error_code?intval($error_code):400);
  if (array_key_exists($error_code, $http_errors))
    $error = $http_errors[intval($error_code)];
  else
    $error = array(
      'pagetitle' => _('Error'),
      'message' => _('An unknown error occurred. If problem persist, please contact support.'),
    );
  http_response_code($error_code);

  $smarty -> assign('message', $error['message']);
  display_template('error_page.tpl', $error['pagetitle']);
  exit();
}

/*
 * Error 404 page
 */

 /**
  * Error 404 handler
  *
  * @param UrlRequest|null $request The request (optional, default: null)
  *
  * @return void
  **/
function error_404($request=null) {
  error_page($request, 404);
}

$_404_url_handler = 'error_404';
function set_404_url_handler($handler=null) {
  global $_404_url_handler;
  $_404_url_handler = $handler;
}


/**
 * Interprets the requested URL and return the corresponding UrlRequest object
 *
 * @param $default_url string|null The default URL if current one does not
 *                                 match with any configured pattern.
 *
 * @return UrlRequest The UrlRequest object corresponding to the the requested URL.
 */
function get_request($default_url=null) {
  global $url_patterns, $_404_url_handler;
  $current_url = get_current_url();
  if ($current_url === false) {
    Log :: fatal(
      _('Unable to determine the requested page. '.
      'If the problem persists, please contact support.')
    );
    exit();
  }
  if (!is_array($url_patterns)) {
    Log :: fatal('URL : No URL patterns configured !');
    exit();
  }

  Log :: debug("URL : current url = '$current_url'");
  Log :: trace(
    "URL : check current url with the following URL patterns :\n - ".
    implode("\n - ", array_keys($url_patterns))
  );
  foreach ($url_patterns as $pattern => $handler_infos) {
    $m = url_match($pattern, $current_url, $handler_infos['methods']);
    if (is_array($m)) {
      $request = new UrlRequest($current_url, $handler_infos, $m);
      // Reset last redirect
      if (isset($_SESSION['last_redirect']))
        unset($_SESSION['last_redirect']);
      Log :: trace("URL : result :\n".vardump($request));
      return $request;
    }
  }
  if ($default_url !== false) {
    Log :: debug("Current url match with no pattern. Redirect to default url ('$default_url')");
    redirect($default_url);
    exit();
  }
  // Error 404
  $api_mode = (strpos($current_url, 'api/') === 0);
  Log :: debug(
    "Current URL match with no pattern. Use error 404 handler (API mode=$api_mode).");
  return new UrlRequest(
    $current_url,
    array(
      'handler' => $_404_url_handler,
      'authenticated' => false,
      'api_mode' => $api_mode,
    )
  );
}

/**
 * Check if the current requested URL match with a specific pattern
 *
 * @param string $pattern The URL pattern
 * @param string|false $current_url The current URL (optional)
 * @param array|null $methods HTTP method (optional, default: no check)
 *
 * @return array|false The URL info if pattern matched, false otherwise.
 **/
function url_match($pattern, $current_url=false, $methods=null) {
  if ($methods && !in_array($_SERVER['REQUEST_METHOD'], $methods))
      return false;
  if ($current_url === false) {
    $current_url = get_current_url();
    if (!$current_url) return False;
  }
  if (preg_match($pattern, $current_url, $m)) {
    Log :: debug(
      "URL : Match found with pattern '$pattern' :\n\t".
      str_replace("\n", "\n\t", print_r($m, true)));
    return $m;
  }
  return False;
}

/**
 * Retreive current requested URL and return it
 *
 * @return string|false The current request URL or false if fail
 **/
function get_current_url() {
  Log :: trace("URL : request URI = '".$_SERVER['REQUEST_URI']."'");

  $base = get_rewrite_base();
  Log :: trace("URL : rewrite base = '$base'");

  if ($_SERVER['REQUEST_URI'] == $base)
    return '';

  if (substr($_SERVER['REQUEST_URI'], 0, strlen($base)) != $base) {
    Log :: error(
      "URL : request URI (".$_SERVER['REQUEST_URI'].") does not start with rewrite base ($base)");
    return False;
  }

  $current_url = substr($_SERVER['REQUEST_URI'], strlen($base));

  // URL contain params ?
  $params_start = strpos($current_url, '?');
  if ($params_start !== false)
    // Params detected, remove it

    // No url / currrent url start by '?' ?
    if ($params_start == 0)
      return '';
    else
      return substr($current_url, 0, $params_start);

  return $current_url;
}

/**
 * Try to detect rewrite base from public root URL
 *
 * @return string The detected rewrite base
 **/
function get_rewrite_base() {
  global $public_root_url;
  if (preg_match('|^https?://[^/]+/(.*)$|', $public_root_url, $m))
    return '/'.remove_trailing_slash($m[1]).'/';
  elseif (preg_match('|^/(.*)$|', $public_root_url, $m))
    return '/'.remove_trailing_slash($m[1]).'/';
  return '/';
}

/**
 * Trigger redirect to specified URL (or homepage if omited)
 *
 * @param string|false $go The destination URL
 *
 * @return void
 **/
function redirect($go=false) {
  global $public_root_url;

  if ($go===false)
    $go = "";
  // If more than one argument passed, format URL using sprintf & urlencode parameters
  elseif (func_num_args() > 1)
    $go = call_user_func_array(
      'sprintf',
      array_merge(
        array($go),
        array_map('urlencode', array_slice(func_get_args(), 1))
      )
    );

  if (is_absolute_url($go))
    $url = $go;
  elseif (isset($public_root_url) && $public_root_url) {
    // Check $public_root_url end
    if (substr($public_root_url, -1)=='/') {
      $public_root_url=substr($public_root_url, 0, -1);
    }
    $url="$public_root_url/$go";
  }
  else
    $url="/$go";

  // Prevent loop
  if (isset($_SESSION['last_redirect']) && $_SESSION['last_redirect'] == $url)
    Log :: fatal(
      _('Unable to determine the requested page (loop detected). '.
      'If the problem persists, please contact support.'));
  else
    $_SESSION['last_redirect'] = $url;

  Log :: debug("redirect($go) => Redirect to : <$url>");
  header("Location: $url");
  exit();
}

/**
 * Handle the current requested URL
 *
 * Note: if the route required that user is authenticated, this method will
 * invoke the force_authentication() special function (or trigger a fatal error
 * if it's not defined).
 *
 * @param string|null $default_url The default URL if current one does not
 *                                 match with any configured pattern.
 *
 * @return void
 **/
function handle_request($default_url=null) {
  global $smarty, $api_mode;

  $sentry_span = new SentrySpan('http.handle_request', 'Handle the HTTP request');

  $request = get_request($default_url);

  if (!is_callable($request -> handler)) {
    Log :: error(
      "URL handler function %s does not exists !",
      format_callable($request -> handler));
    Log :: fatal(_("This request cannot be processed."));
  }

  if ($request -> api_mode)
    $api_mode = true;
  if (isset($smarty) && $smarty)
    $smarty -> assign('request', $request);

  // Check authentication (if need)
  if($request -> authenticated)
    if (function_exists('force_authentication'))
      force_authentication();
    else
      Log :: fatal(_("Authentication required but force_authentication function is not defined."));

  try {
    call_user_func($request -> handler, $request);
  }
  catch (Exception $e) {
    Log :: exception(
      $e, "An exception occured running URL handler function ".$request -> handler."()");
    Log :: fatal(_("This request could not be processed correctly."));
  }
  $sentry_span->finish();
}

/**
 * Remove trailing slash in specified URL
 *
 * @param string $url The URL
 *
 * @return string The specified URL without trailing slash
 **/
function remove_trailing_slash($url) {
  if ($url == '/')
    return $url;
  elseif (substr($url, -1) == '/')
    return substr($url, 0, -1);
  return $url;
}

/**
 * Check an AJAX request and trigger a fatal error on fail
 *
 * Check if session key is present and valid and set AJAX
 * mode.
 *
 * @param string|null $session_key string The current session key (optional)
 *
 * @return void
 **/
function check_ajax_request($session_key=null) {
  global $ajax, $debug_ajax;
  $ajax = true;

  if (check_session_key($session_key))
    fatal_error('Invalid request');

  if ($debug_ajax)
    Log :: debug("Ajax Request : ".vardump($_REQUEST));
}

/**
 * Get the public absolute URL
 *
 * @param string|null $relative_url Relative URL to convert (Default: current URL)
 *
 * @return string The public absolute URL
 **/
function get_absolute_url($relative_url=null) {
  global $public_root_url;
  if (!is_string($relative_url))
      $relative_url = get_current_url();
  if ($public_root_url[0] == '/') {
      Log :: debug(
        "URL :: get_absolute_url($relative_url): configured public root URL is relative ".
        "($public_root_url) => try to detect it from current request infos.");
      $public_root_url = (
        'http'.(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'?'s':'').'://'.
        $_SERVER['HTTP_HOST'].$public_root_url);
      Log :: debug(
        "URL :: get_absolute_url($relative_url): detected public_root_url: $public_root_url");
  }

  if (substr($relative_url, 0, 1) == '/')
    $relative_url = substr($relative_url, 1);
  $url = remove_trailing_slash($public_root_url)."/$relative_url";
  Log :: debug("URL :: get_absolute_url($relative_url): result = $url");
  return $url;
}

/**
 * Check if specified URL is absolute
 *
 * @param string $url The URL to check
 *
 * @return boolean True if specified URL is absolute, False otherwise
 **/
function is_absolute_url($url) {
  return boolval(preg_match('#^https?://#', $url));
}

/**
 * Add parameter in specified URL
 *
 * @param string &$url The reference of the URL
 * @param string $param The parameter name
 * @param string $value The parameter value
 * @param boolean $encode Set if parameter value must be URL encoded (optional, default: true)
 *
 * @return string The completed URL
 */
function add_url_parameter(&$url, $param, $value, $encode=true) {
  if (strpos($url, '?') === false)
    $url .= '?';
  else
    $url .= '&';
  $url .= "$param=".($encode?urlencode($value):$value);
  return $url;
}

/**
 * URL request abstraction
 *
 * @author Benjamin Renard <brenard@easter-eggs.com>
 */
class UrlRequest {

  // The URL requested handler
  private $current_url = null;

  // The URL requested handler
  private $handler = null;

  // Request need authentication ?
  private $authenticated = true;

  // API mode enabled ?
  private $api_mode = false;

  // Parameters detected on requested URL
  private $url_params = array();

  public function __construct($current_url, $handler_infos, $url_params=array()) {
    $this -> current_url = $current_url;
    $this -> handler = $handler_infos['handler'];
    $this -> authenticated = (
      isset($handler_infos['authenticated'])?
      boolval($handler_infos['authenticated']):true);
    $this -> api_mode = (
      isset($handler_infos['api_mode'])?
      boolval($handler_infos['api_mode']):false);
    $this -> url_params = $url_params;
  }

  /**
   * Get request info
   *
   * @param string $key The name of the info
   *
   * @return mixed The value
   **/
  public function __get($key) {
    if ($key == 'current_url')
      return $this -> current_url;
    if ($key == 'handler')
      return $this -> handler;
    if ($key == 'authenticated')
      return $this -> authenticated;
    if ($key == 'api_mode')
      return $this -> api_mode;
    if ($key == 'referer')
      return $this -> get_referer();
    if ($key == 'http_method')
      return $_SERVER['REQUEST_METHOD'];
    if (array_key_exists($key, $this->url_params)) {
      return urldecode($this->url_params[$key]);
    }
    // Unknown key, log warning
    Log :: warning(
      "__get($key): invalid property requested\n%s",
      Log :: get_debug_backtrace_context());
  }

  /**
   * Set request info
   *
   * @param string $key The name of the info
   * @param mixed $value The value of the info
   *
   * @return void
   **/
  public function __set($key, $value) {
    if ($key == 'referer')
      $_SERVER['HTTP_REFERER'] = $value;
    elseif ($key == 'http_method')
      $_SERVER['REQUEST_METHOD'] = $value;
    else
      $this->url_params[$key] = $value;
  }



  /**
   * Check is request info is set
   *
   * @param string $key The name of the info
   *
   * @return bool True is info is set, False otherwise
   **/
  public function __isset($key) {
    if (
      in_array(
        $key, array('current_url', 'handler', 'authenticated',
       'api_mode', 'referer', 'http_method')
      )
    )
      return True;
    return array_key_exists($key, $this->url_params);
  }

  /**
   * Get request parameter
   *
   * @param string $parameter The name of the parameter
   * @param bool $decode If true, the parameter value will be urldecoded
   *                     (optional, default: true)
   *
   * @return mixed The value or false if parameter does not exists
   **/
  public function get_param($parameter, $decode=true) {
    if (array_key_exists($parameter, $this->url_params)) {
      if ($decode)
        return urldecode($this->url_params[$parameter]);
      return $this->url_params[$parameter];
    }
    return false;
  }

  /**
   * Get request referer (if known)
   *
   * @return string|null The request referer URL if known, null otherwise
   */
  public function get_referer() {
    if (isset($_SERVER['HTTP_REFERER']) && $_SERVER['HTTP_REFERER'])
      return $_SERVER['HTTP_REFERER'];
    return null;
  }

}

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