chevereto-free/lib/G/classes/class.handler.php

590 lines
18 KiB
PHP

<?php
/* --------------------------------------------------------------------
G\ library
https://g.chevereto.com
@author Rodolfo Berrios A. <http://rodolfoberrios.com/>
Copyright (c) Rodolfo Berrios <inbox@rodolfoberrios.com> All rights reserved.
Licensed under the MIT license
http://opensource.org/licenses/MIT
--------------------------------------------------------------------- */
/**
* class.handler.php
* This class does all the route handling process of the G\ app
*/
namespace G;
use Exception;
class Handler
{
public static $route;
public static $route_request;
public static $route_name;
public static $base_request;
public static $doctitle;
public static $vars;
public static $conds;
public static $routes;
public static $template_used;
public static $prevented_route;
public static $mapped_args;
/**
* Build a valid request
*/
public function __construct($hook = [])
{
if (!defined('G_APP_PATH_THEME')) {
throw new HandlerException('G_APP_PATH_THEME is not defined', 100);
}
// Parse the definitions to this object.. This is not necessary but in case of changes...
$this->relative_root = G_ROOT_PATH_RELATIVE; // nota: realmente necesitamos estos this?
$this->base_url = get_root_url();
$this->path_theme = G_APP_PATH_THEME;
// Parse the request
$this->request_uri = $_SERVER['REQUEST_URI'];
$this->script_name = $_SERVER['SCRIPT_NAME'];
$query_string = '?' . $_SERVER['QUERY_STRING'];
if (!empty($_SERVER['QUERY_STRING'])) {
$this->request_uri = str_replace($query_string, '/', $this->request_uri);
}
$this->valid_request = '/' . ltrim(rtrim(sanitize_path_slashes($this->request_uri), '/'), '/');
if (!empty($_SERVER['QUERY_STRING'])) {
$this->request_uri = $_SERVER['REQUEST_URI'];
$this->valid_request .= /*'/' .*/ $query_string;
}
// Store the canonical request, used for redirect to a valid request
$this->canonical_request = $this->valid_request;
if (is_dir(G_ROOT_PATH . $this->valid_request) && $this->valid_request !== '/') {
$this->canonical_request .= '/';
}
$this->handled_request = strtok($this->relative_root == '/' ? $this->valid_request : preg_replace('#' . $this->relative_root . '#', '/', $this->request_uri, 1), '?');
$this->request_array = explode('/', rtrim(str_replace('//', '/', ltrim($this->handled_request, '/')), '/'));
// Index request
if ($this->request_array[0] == '') {
$this->request_array[0] = '/';
}
$this->request_array = array_values(array_filter($this->request_array, 'strlen'));
self::$base_request = $this->request_array[0];
// Reserved route (index)
if (self::$base_request == 'index') {
redirect('/', 301);
}
// Fix the canonical request /something?q= to /something/?q=
if (self::$base_request !== '' && !empty($_SERVER['QUERY_STRING'])) {
$path_request = add_trailing_slashes(rtrim(str_replace($_SERVER['QUERY_STRING'], '', $this->canonical_request), '?'));
$fixed_qs_request = $path_request . '?' . $_SERVER['QUERY_STRING'];
$this->canonical_request = $fixed_qs_request;
}
// No /index.php request
if (self::$base_request == 'index.php') {
$this->canonical_request = rtrim($this->canonical_request, '/');
redirect((sanitize_path_slashes(str_replace('index.php', '', $this->canonical_request))), 301);
}
// If the request is invalid we make a 301 redirection to the canonical url.
if ($this->relative_root !== $this->request_uri and $this->canonical_request !== $this->request_uri) {
$this->baseRedirection($this->canonical_request);
}
if (in_array(self::$base_request, ['', 'index.php', '/'])) {
self::$base_request = 'index';
}
$this->template = self::$base_request;
$this->request = $this->request_array;
self::$route_request = $this->request_array;
self::$route = $this->template !== 404 ? $this->request_array[0] == '/' ? 'index' : $this->request_array : 404;
unset($this->request[0]);
$this->request = array_values($this->request);
// Hook a fn BEFORE the process
if (is_array($hook) and is_callable($hook['before'])) {
$hook['before']($this);
}
// It is a valid request on index.php?
if ($this->isIndex()) {
$this->processRequest();
}
// Hook a fn AFTER the process
if (is_array($hook) and is_callable($hook['after'])) {
$hook['after']($this);
}
// Auto-bind the route vars
if (is_array(self::$vars)) {
foreach (self::$vars as $k => $v) {
$this->bindGetFn($k, $v);
}
}
// Auto-bind the route conditionals
if (is_array(self::$conds)) {
foreach (self::$conds as $k => $v) {
$this->bindIsFn($k, $v);
}
}
$this->loadTemplate();
}
/**
* Iterate over the route app folder
* This populates Handler::$routes with all the valid routes
*/
private static function routeIterator($path)
{
if (!file_exists($path)) {
return;
}
foreach (new \DirectoryIterator($path) as $fileInfo) {
if ($fileInfo->isDot() or $fileInfo->isDir()) {
continue;
}
$route_file = $path . $fileInfo->getFilename();
$route_override = $path . 'overrides/' . $fileInfo->getFilename();
if (file_exists($route_override)) {
$route_file = $route_override;
}
if (file_exists($route_file)) {
require_once($route_file);
$route = array(substr(substr($fileInfo->getFilename(), 0, -4), 6) => $route);
self::$routes += $route;
}
}
}
/**
* Stock (save) the valid routes of the G\ app
* This method is optional because the routeIterator takes some memory
*/
public static function stockRoutes()
{
self::$routes = [];
self::routeIterator(G_APP_PATH_ROUTES);
self::routeIterator(G_APP_PATH_ROUTES_OVERRIDES);
}
/**
* Process the dynamic request
*/
private function processRequest()
{
if (is_null(self::$routes)) { // Route array is not set
$route = $this->getRouteFn(self::$base_request);
if (is_callable($route)) {
$routes[self::$base_request] = $route; // Build a single $routes array
}
} else {
$routes = self::$routes;
}
if (is_array($routes) and array_key_exists(self::$base_request, $routes)) {
// Autoset some magic
$magic = array(
'post' => $_POST ? $_POST : null,
'get' => $_GET ? $_GET : null,
'request' => $_REQUEST ? $_REQUEST : null,
'safe_post' => $_POST ? safe_html($_POST) : null,
'safe_get' => $_GET ? safe_html($_GET) : null,
'safe_request' => $_REQUEST ? safe_html($_REQUEST) : null,
'auth_token' => self::getAuthToken()
);
if (self::$vars && count(self::$vars) > 0) {
self::$vars = array_merge(self::$vars, $magic);
} else {
self::$vars = $magic;
}
// Only call a valid route fn
if (!self::$prevented_route and is_callable($routes[self::$base_request])) {
$routes[self::$base_request]($this);
}
} else {
$this->issue404();
$this->request = $this->request_array;
}
if ($this->template == 404) {
self::$route = 404;
}
self::setCond('404', $this->template == 404); // is_404 binding
if (self::$vars['pre_doctitle']) {
$stock_doctitle = self::$vars['doctitle'];
self::$vars['doctitle'] = self::$vars['pre_doctitle'];
if ($stock_doctitle) {
self::$vars['doctitle'] .= ' - ' . $stock_doctitle;
}
}
self::$template_used = $this->template;
}
/**
* Bind route var to global functions
*/
public function bindGetFn($var, $value)
{
$fn_name = strtolower(str_replace('-', '_', $var));
if (!function_exists('get_' . $fn_name)) {
eval('function get_' . $fn_name . '(){ return G\Handler::$vars["' . $var . '"]; }');
}
}
/**
* Bind route conditional to global functions
*/
public function bindIsFn($var, $value)
{
$fn_name = strtolower(str_replace('-', '_', $var));
if (!function_exists('is_' . $fn_name)) {
eval('function is_' . $fn_name . '(){ return G\Handler::$conds["' . $var . '"]; }');
}
}
/**
* Inject the 404 page
*/
public function issue404()
{
set_status_header(404);
if ($this->getCond('mapped_route')) {
self::$base_request = self::$route_request[0];
self::$route_name = 404;
}
$this->is404 = true;
$this->template = 404;
}
/**
* Prevent the rest of the execution loading the target view
*/
public function preventRoute($tpl = null)
{
if ($tpl) {
$this->template = $tpl;
}
self::$prevented_route = true;
}
/**
* Get the route fn for a given route
* If the route doesn't exists it will add it to the routes stack
*/
public function getRouteFn($route_name)
{
// Route is already in the stack
if (is_array(self::$routes) and array_key_exists($route_name, Handler::$routes)) {
return self::$routes[$route_name];
}
// Route doesn't exists in the stack
$filename = 'route.' . $route_name . '.php';
$route_file = G_APP_PATH_ROUTES . $filename;
$route_override_file = G_APP_PATH_ROUTES_OVERRIDES . $filename;
if (file_exists($route_override_file)) {
$route_file = $route_override_file;
}
if (file_exists($route_file)) {
require($route_file);
// Append this new route fn to the Handler::$routes stack
self::$routes[$route_name] = $route;
self::$route_name = $route_name;
return $route;
} else {
return false;
}
}
/**
* Maps the current route which is useful to make route aliases
*/
public function mapRoute($route_name, $args = null)
{
$this->template = $route_name;
self::$base_request = $route_name;
self::setCond('mapped_route', true);
if (!is_null($args)) {
self::$mapped_args = $args;
}
return $this->getRouteFn($route_name);
}
/**
* Return (bool) the request level of the current request
*/
public function isRequestLevel($level)
{
return isset($this->request_array[$level - 1]);
}
/**
* Redirect to the base url/request
*/
public function baseRedirection($request)
{
$request = trim(sanitize_path_slashes($request), '/');
$url = preg_replace('{' . $this->relative_root . '}', '/', $this->base_url, 1) . $request;
redirect($url, 301);
}
/**
* Return (bool) if the request is handled by index.php
*/
private function isIndex()
{
return preg_match('{\/index\.php$}', $this->script_name);
}
/**
* Hook code for loadTemplate()
* @args ['code' => '<code>', 'where' => 'before|after']
*/
public function hookTemplate($args = [])
{
if (in_array($args['where'], ['before', 'after']) and $args['code']) {
if (!isset($this->hook_template)) {
$this->hook_template = [];
}
$this->hook_template[$args['where']] = $args['code'];
}
}
/**
* load the setted (or argument) template view
*/
private function loadTemplate($template = null)
{
if (!is_null($template)) {
$this->template = $template;
}
/** Overrides are loaded from highest to lowest priority **/
$functions_basename = 'functions.php';
$template_functions = [
$this->path_theme . 'overrides/' . $functions_basename,
$this->path_theme . $functions_basename
];
foreach ($template_functions as $file) {
if (file_exists($file)) {
require_once($file);
break;
}
}
$view_basename = $this->template;
$view_extension = get_file_extension($this->template);
if (!$view_extension) {
$view_extension = 'php';
$view_basename .= '.php';
}
$template_file = [
$this->path_theme . 'overrides/views/' . $view_basename,
$this->path_theme . 'overrides/' . $view_basename,
$this->path_theme . 'views/' . $view_basename,
$this->path_theme . $view_basename,
];
foreach ($template_file as $file) {
if (file_exists($file)) {
if ($view_extension == 'html') {
Render\include_theme_header();
}
if ($this->hook_template['before']) {
echo $this->hook_template['before'];
}
if ($view_extension == 'php') {
require_once($file);
} else {
echo file_get_contents($file);
}
if ($this->hook_template['after']) {
echo $this->hook_template['after'];
}
if ($view_extension == 'html') {
Render\include_theme_footer();
}
return;
}
}
$end = end($template_file);
$key = key($template_file);
throw new HandlerException('Missing ' . absolute_to_relative($template_file[$key]) . ' template file', 400);
}
/**
* Returns the 40 char length safe request token
*/
public static function getAuthToken()
{
$token = isset($_SESSION['G_auth_token']) ? $_SESSION['G_auth_token'] : random_string(40);
$_SESSION['G_auth_token'] = $token;
return $token;
}
/**
* Checks the integrity and validation of the given request token
*/
public static function checkAuthToken($token)
{
if (strlen($token) < 40) {
return false;
}
return timing_safe_compare($_SESSION['G_auth_token'], $token);
}
/**
* Sets a Handler::$var > get_var() binding
*/
public static function setVar($var, $value)
{
self::$vars[$var] = $value;
}
/**
* Sets a multiple Handler::$var > get_var() binding
*/
public static function setVars($array)
{
foreach ((array) $array as $var => $value) {
self::$vars[$var] = $value;
}
}
/**
* Sets a Handler::$conds -> is_cond() binding
*/
public static function setCond($conds, $bool)
{
self::$conds[$conds] = !$bool ? false : true;
}
/**
* Sets a multiple Handler::$conds -> is_cond() binding
*/
public static function setConds($array = [])
{
foreach ((array) $array as $conds => $bool) {
self::$conds[$conds] = !$bool ? false : true;
}
}
/**
* Get a Handler::$vars[var]
*/
public static function getVar($var)
{
return self::getVars()[$var];
}
/**
* Get all Handler::$vars
*/
public static function getVars()
{
return self::$vars;
}
/**
* Get a Handler::$condss[cond]
*/
public static function getCond($cond)
{
return self::getConds()[$cond];
}
/**
* Get all Handler::$conds
*/
public static function getConds()
{
return self::$conds;
}
/**
* Smart update a Handler::$vars
*/
public static function updateVar($var, $value)
{
if (is_array(self::$vars[$var]) and is_array($value)) {
//self::$vars[$var] = array_merge(self::$vars[$var], $value);
$value += self::$vars[$var]; // replacement + replaced
ksort($value);
}
self::$vars[$var] = $value;
}
/**
* Unset a given var
*/
public static function unsetVar($var)
{
unset(self::$vars[$var]);
}
/**
* Get the template file basename used
*/
public static function getTemplateUsed()
{
return self::$template_used;
}
/**
* Get the current route path
* @args $full (bool true) outputs the full route 'like/this' or 'this'
*/
public static function getRoutePath($full = true)
{
if (is_array(self::$route)) {
return $full ? implode('/', self::$route) : self::$route[0];
} else {
return self::$route;
}
}
/**
* Get the current route name from route.name.php
*/
public static function getRouteName()
{
return self::$route_name;
}
}
class HandlerException extends Exception
{ }