diff options
Diffstat (limited to 'localwebsite/engine')
-rw-r--r-- | localwebsite/engine/debug.php | 355 | ||||
-rw-r--r-- | localwebsite/engine/request_handler.php | 142 | ||||
-rw-r--r-- | localwebsite/engine/router.php | 199 | ||||
-rw-r--r-- | localwebsite/engine/tpl.php | 520 |
4 files changed, 1216 insertions, 0 deletions
diff --git a/localwebsite/engine/debug.php b/localwebsite/engine/debug.php new file mode 100644 index 0000000..b1b959f --- /dev/null +++ b/localwebsite/engine/debug.php @@ -0,0 +1,355 @@ +<?php + +// require_once 'engine/mysql.php'; + +class debug { + + protected static $Types = [ + 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', + 2048 => 'E_STRICT', + 4096 => 'E_RECOVERABLE_ERROR', + 8192 => 'E_DEPRECATED', + 16384 => 'E_USER_DEPRECATED', + 32767 => 'E_ALL' + ]; + + const STORE_NONE = -1; + const STORE_MYSQL = 0; + const STORE_FILE = 1; + const STORE_BOTH = 2; + + private static $instance = null; + + protected $enabled = false; + protected $errCounter = 0; + protected $logCounter = 0; + protected $messagesStoreType = self::STORE_NONE; + protected $errorsStoreType = self::STORE_NONE; + protected $filter; + protected $reportRecursionLevel = 0; + protected $overridenDebugFile = null; + protected $silent = false; + protected $prefix; + + private function __construct($filter) { + $this->filter = $filter; + } + + public static function getInstance($filter = null) { + if (is_null(self::$instance)) { + self::$instance = new self($filter); + } + return self::$instance; + } + + public function enable() { + $self = $this; + + set_error_handler(function($no, $str, $file, $line) use ($self) { + if ($self->silent || !$self->enabled) { + return; + } + if ((is_callable($this->filter) && !($this->filter)($no, $file, $line, $str)) || !$self->canReport()) { + return; + } + $self->report(true, $str, $no, $file, $line); + }); + + append_shutdown_function(function() use ($self) { + if (!$self->enabled || !($error = error_get_last())) { + return; + } + if (is_callable($this->filter) + && !($this->filter)($error['type'], $error['file'], $error['line'], $error['message'])) { + return; + } + if (!$self->canReport()) { + return; + } + $self->report(true, $error['message'], $error['type'], $error['file'], $error['line']); + }); + + $this->enabled = true; + } + + public function disable() { + restore_error_handler(); + $this->enabled = false; + } + + public function report($is_error, $text, $errno = 0, $errfile = '', $errline = '') { + global $config; + + $this->reportRecursionLevel++; + + $logstarted = $this->errCounter > 0 || $this->logCounter > 0; + $num = $is_error ? $this->errCounter++ : $this->logCounter++; + $custom = $is_error && !$errno; + $ts = time(); + $exectime = exectime(); + $bt = backtrace(2); + + $store_file = (!$is_error && $this->checkMessagesStoreType(self::STORE_FILE)) + || ($is_error && $this->checkErrorsStoreType(self::STORE_FILE)); + + $store_mysql = (!$is_error && $this->checkMessagesStoreType(self::STORE_MYSQL)) + || ($is_error && $this->checkErrorsStoreType(self::STORE_MYSQL)); + + if ($this->prefix) + $text = $this->prefix.$text; + + // if ($store_mysql) { + // $db = getMySQL('local_logs', true); + // $data = [ + // 'ts' => $ts, + // 'num' => $num, + // 'time' => $exectime, + // 'custom' => intval($custom), + // 'errno' => $errno, + // 'file' => $errfile, + // 'line' => $errline, + // 'text' => $text, + // 'stacktrace' => $bt, + // 'is_cli' => PHP_SAPI == 'cli' ? 1 : 0, + // ]; + // if (PHP_SAPI == 'cli') { + // $data += [ + // 'ip' => '', + // 'ua' => '', + // 'url' => '', + // ]; + // } else { + // $data += [ + // 'ip' => ip2ulong($_SERVER['REMOTE_ADDR']), + // 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '', + // 'url' => $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] + // ]; + // } + // $db->insert('backend_errors', $data); + // } + + if ($store_file) { + $title = PHP_SAPI == 'cli' ? 'cli' : $_SERVER['REQUEST_URI']; + $date = date('d/m/y H:i:s', $ts); + $exectime = (string)$exectime; + if (strlen($exectime) < 6) + $exectime .= str_repeat('0', 6 - strlen($exectime)); + + $buf = ""; + if (!$logstarted) { + $buf .= "\n<e fg=white bg=magenta style=fgbright,bold> {$title} </e><e fg=white bg=blue style=fgbright> {$date} </e>\n"; + } + $buf .= "<e fg=".($is_error ? 'red' : 'white').">".($is_error ? 'E' : 'I')."=<e style=bold>${num}</e> <e fg=cyan>{$exectime}</e> "; + if ($is_error && !$custom) { + $buf .= "<e fg=green>{$errfile}<e fg=white>:<e fg=green style=fgbright>{$errline}</e> (".self::errname($errno).") "; + } + $buf = stransi($buf); + + $buf .= $text; + $buf .= "\n"; + if ($is_error && $config['debug_backtrace']) { + $buf .= $bt."\n"; + } + + $debug_file = $this->getDebugFile(); + + $logdir = dirname($debug_file); + if (!file_exists($logdir)) { + mkdir($logdir); + setperm($logdir); + } + + $f = fopen($debug_file, 'a'); + if ($f) { + fwrite($f, $buf); + fclose($f); + } + } + + $this->reportRecursionLevel--; + } + + public function canReport() { + return $this->reportRecursionLevel < 2; + } + + public function setErrorsStoreType($errorsStoreType) { + $this->errorsStoreType = $errorsStoreType; + } + + public function setMessagesStoreType($messagesStoreType) { + $this->messagesStoreType = $messagesStoreType; + } + + public function checkMessagesStoreType($store_type) { + return $this->messagesStoreType == $store_type || $this->messagesStoreType == self::STORE_BOTH; + } + + public function checkErrorsStoreType($store_type) { + return $this->errorsStoreType == $store_type || $this->errorsStoreType == self::STORE_BOTH; + } + + public function overrideDebugFile($file) { + $this->overridenDebugFile = $file; + } + + protected function getDebugFile() { + global $config; + return is_null($this->overridenDebugFile) ? ROOT.'/'.$config['debug_file'] : $this->overridenDebugFile; + } + + public function setSilence($silent) { + $this->silent = $silent; + } + + public function setPrefix($prefix) { + $this->prefix = $prefix; + } + + public static function errname($errno) { + static $errors = null; + if (is_null($errors)) { + $errors = array_flip(array_slice(get_defined_constants(true)['Core'], 0, 15, true)); + } + return $errors[$errno]; + } + + public static function getTypes() { + return self::$Types; + } + +} + +class debug_measure { + + private $name; + private $time; + private $started = false; + + /** + * @param string $name + * @return $this + */ + public function start($name = null) { + if (is_null($name)) { + $name = strgen(3); + } + $this->name = $name; + $this->time = microtime(true); + $this->started = true; + return $this; + } + + /** + * @return float|string|null + */ + public function finish() { + if (!$this->started) { + debugLog("debug_measure::finish(): not started, name=".$this->name); + return null; + } + + $time = (microtime(true) - $this->time); + debugLog("MEASURE".($this->name != '' ? ' '.$this->name : '').": ".$time); + + $this->started = false; + return $time; + } + +} + +/** + * @param $var + * @return string + */ +function str_print_r($var) { + ob_start(); + print_r($var); + return trim(ob_get_clean()); +} + +/** + * @param $var + * @return string + */ +function str_var_dump($var) { + ob_start(); + var_dump($var); + return trim(ob_get_clean()); +} + +/** + * @param $args + * @param bool $all_dump + * @return string + */ +function str_vars($args, $all_dump = false) { + return implode(' ', array_map(function($a) use ($all_dump) { + if ($all_dump) { + return str_var_dump($a); + } + $type = gettype($a); + if ($type == 'string' || $type == 'integer' || $type == 'double') { + return $a; + } else if ($type == 'array' || $type == 'object') { + return str_print_r($a); + } else { + return str_var_dump($a); + } + }, $args)); +} + +/** + * @param int $shift + * @return string + */ +function backtrace($shift = 0) { + $bt = debug_backtrace(); + $lines = []; + foreach ($bt as $i => $t) { + if ($i < $shift) { + continue; + } + if (!isset($t['file'])) { + $lines[] = 'from ?'; + } else { + $lines[] = 'from '.$t['file'].':'.$t['line']; + } + } + return implode("\n", $lines); +} + +/** + * @param mixed ...$args + */ +function debugLog(...$args) { + global $config; + if (!$config['is_dev']) + return; + + debug::getInstance()->report(false, str_vars($args)); +} + +function debugLogOnProd(...$args) { + debug::getInstance()->report(false, str_vars($args)); +} + +/** + * @param mixed ...$args + */ +function debugError(...$args) { + $debug = debug::getInstance(); + if ($debug->canReport()) { + $debug->report(true, str_vars($args)); + } +} diff --git a/localwebsite/engine/request_handler.php b/localwebsite/engine/request_handler.php new file mode 100644 index 0000000..535e850 --- /dev/null +++ b/localwebsite/engine/request_handler.php @@ -0,0 +1,142 @@ +<?php + +abstract class request_handler { + + const GET = 'GET'; + const POST = 'POST'; + + private static array $AllowedInputTypes = ['i', 'f', 'b', 'e' /* enum */]; + + public function dispatch(string $act) { + $method = $_SERVER['REQUEST_METHOD'] == 'POST' ? 'POST' : 'GET'; + return $this->call_act($method, $act); + } + + protected function before_dispatch(string $method, string $act)/*: ?array*/ { + return null; + } + + protected function call_act(string $method, string $act, array $input = []) { + global $RouterInput; + + $notfound = !method_exists($this, $method.'_'.$act) || !((new ReflectionMethod($this, $method.'_'.$act))->isPublic()); + if ($notfound) + $this->method_not_found($method, $act); + + if (!empty($input)) { + foreach ($input as $k => $v) + $RouterInput[$k] = $v; + } + + $args = $this->before_dispatch($method, $act); + return call_user_func_array([$this, $method.'_'.$act], is_array($args) ? [$args] : []); + } + + abstract protected function method_not_found(string $method, string $act); + + protected function input(string $input, bool $as_assoc = false): array { + $input = preg_split('/,\s+?/', $input, null, PREG_SPLIT_NO_EMPTY); + + $ret = []; + foreach ($input as $var) { + list($type, $name, $enum_values, $enum_default) = self::parse_input_var($var); + + $value = param($name); + + switch ($type) { + case 'i': + if (is_null($value) && !is_null($enum_default)) { + $value = (int)$enum_default; + } else { + $value = (int)$value; + } + break; + + case 'f': + if (is_null($value) && !is_null($enum_default)) { + $value = (float)$enum_default; + } else { + $value = (float)$value; + } + break; + + case 'b': + if (is_null($value) && !is_null($enum_default)) { + $value = (bool)$enum_default; + } else { + $value = (bool)$value; + } + break; + + case 'e': + if (!in_array($value, $enum_values)) { + $value = !is_null($enum_default) ? $enum_default : ''; + } + break; + } + + if (!$as_assoc) { + $ret[] = $value; + } else { + $ret[$name] = $value; + } + } + + return $ret; + } + protected static function parse_input_var(string $var): array { + $type = null; + $name = null; + $enum_values = null; + $enum_default = null; + + $pos = strpos($var, ':'); + if ($pos !== false) { + $type = substr($var, 0, $pos); + $rest = substr($var, $pos+1); + + if (!in_array($type, self::$AllowedInputTypes)) { + trigger_error('request_handler::parse_input_var('.$var.'): unknown type '.$type); + $type = null; + } + + switch ($type) { + case 'e': + $br_from = strpos($rest, '('); + $br_to = strpos($rest, ')'); + + if ($br_from === false || $br_to === false) { + trigger_error('request_handler::parse_input_var('.$var.'): failed to parse enum values'); + $type = null; + $name = $rest; + break; + } + + $enum_values = array_map('trim', explode('|', trim(substr($rest, $br_from+1, $br_to-$br_from-1)))); + $name = trim(substr($rest, 0, $br_from)); + + if (!empty($enum_values)) foreach ($enum_values as $key => $val) { + if (substr($val, 0, 1) == '=') { + $enum_values[$key] = substr($val, 1); + $enum_default = $enum_values[$key]; + } + } + break; + + default: + if (($eq_pos = strpos($rest, '=')) !== false) { + $enum_default = substr($rest, $eq_pos+1); + $rest = substr($rest, 0, $eq_pos); + } + $name = trim($rest); + break; + } + } else { + $type = 's'; + $name = $var; + } + + return [$type, $name, $enum_values, $enum_default]; + } + +} diff --git a/localwebsite/engine/router.php b/localwebsite/engine/router.php new file mode 100644 index 0000000..5e966a9 --- /dev/null +++ b/localwebsite/engine/router.php @@ -0,0 +1,199 @@ +<?php + +class router { + + protected array $routes = [ + 'children' => [], + 're_children' => [] + ]; + + public function add($template, $value) { + if ($template == '') { + return; + } + + // expand {enum,erat,ions} + $templates = [[$template, $value]]; + if (preg_match_all('/\{([\w\d_\-,]+)\}/', $template, $matches)) { + foreach ($matches[1] as $match_index => $variants) { + $variants = explode(',', $variants); + $variants = array_map('trim', $variants); + $variants = array_filter($variants, function($s) { return $s != ''; }); + + for ($i = 0; $i < count($templates); ) { + list($template, $value) = $templates[$i]; + $new_templates = []; + foreach ($variants as $variant_index => $variant) { + $new_templates[] = [ + str_replace_once($matches[0][$match_index], $variant, $template), + str_replace('${'.($match_index+1).'}', $variant, $value) + ]; + } + array_splice($templates, $i, 1, $new_templates); + $i += count($new_templates); + } + } + } + + // process all generated routes + foreach ($templates as $template) { + list($template, $value) = $template; + + $start_pos = 0; + $parent = &$this->routes; + $template_len = strlen($template); + + while ($start_pos < $template_len) { + $slash_pos = strpos($template, '/', $start_pos); + if ($slash_pos !== false) { + $part = substr($template, $start_pos, $slash_pos-$start_pos+1); + $start_pos = $slash_pos+1; + } else { + $part = substr($template, $start_pos); + $start_pos = $template_len; + } + + $parent = &$this->_addRoute($parent, $part, + $start_pos < $template_len ? null : $value); + } + } + } + + protected function &_addRoute(&$parent, $part, $value = null) { + $par_pos = strpos($part, '('); + $is_regex = $par_pos !== false && ($par_pos == 0 || $part[$par_pos-1] != '\\'); + + $children_key = !$is_regex ? 'children' : 're_children'; + + if (isset($parent[$children_key][$part])) { + if (is_null($value)) { + $parent = &$parent[$children_key][$part]; + } else { + if (!isset($parent[$children_key][$part]['value'])) { + $parent[$children_key][$part]['value'] = $value; + } else { + trigger_error(__METHOD__.': route is already defined'); + } + } + return $parent; + } + + $child = [ + 'children' => [], + 're_children' => [] + ]; + if (!is_null($value)) { + $child['value'] = $value; + } + + $parent[$children_key][$part] = $child; + return $parent[$children_key][$part]; + } + + public function find($uri) { + if ($uri != '/' && $uri[0] == '/') { + $uri = substr($uri, 1); + } + $start_pos = 0; + $parent = &$this->routes; + $uri_len = strlen($uri); + $matches = []; + + while ($start_pos < $uri_len) { + $slash_pos = strpos($uri, '/', $start_pos); + if ($slash_pos !== false) { + $part = substr($uri, $start_pos, $slash_pos-$start_pos+1); + $start_pos = $slash_pos+1; + } else { + $part = substr($uri, $start_pos); + $start_pos = $uri_len; + } + + $found = false; + if (isset($parent['children'][$part])) { + $parent = &$parent['children'][$part]; + $found = true; + } else if (!empty($parent['re_children'])) { + foreach ($parent['re_children'] as $re => &$child) { + $exp = '#^'.$re.'$#'; + $re_result = preg_match($exp, $part, $match); + if ($re_result === false) { + debugError(__METHOD__.": regex $exp failed"); + continue; + } + + if ($re_result) { + if (count($match) > 1) { + $matches = array_merge($matches, array_slice($match, 1)); + } + $parent = &$child; + $found = true; + break; + } + } + } + + if (!$found) { + return false; + } + } + + if (!isset($parent['value'])) { + return false; + } + + $value = $parent['value']; + if (!empty($matches)) { + foreach ($matches as $i => $match) { + $needle = '$('.($i+1).')'; + $pos = strpos($value, $needle); + if ($pos !== false) { + $value = substr_replace($value, $match, $pos, strlen($needle)); + } + } + } + + return $value; + } + + public function load($routes) { + $this->routes = $routes; + } + + public function dump() { + return $this->routes; + } + +} + +function routerFind(router $router) { + $document_uri = $_SERVER['REQUEST_URI']; + if (($pos = strpos($document_uri, '?')) !== false) + $document_uri = substr($document_uri, 0, $pos); + $document_uri = urldecode($document_uri); + + $fixed_document_uri = preg_replace('#/+#', '/', $document_uri); + if ($fixed_document_uri != $document_uri && !is_xhr_request()) { + redirect($fixed_document_uri); + } else { + $document_uri = $fixed_document_uri; + } + + $route = $router->find($document_uri); + if ($route === false) + return false; + + $route = preg_split('/ +/', $route); + $handler = $route[0]; + $act = $route[1]; + $input = []; + if (count($route) > 2) { + for ($i = 2; $i < count($route); $i++) { + $var = $route[$i]; + list($k, $v) = explode('=', $var); + $input[trim($k)] = trim($v); + } + } + + return [$handler, $act, $input]; +}
\ No newline at end of file diff --git a/localwebsite/engine/tpl.php b/localwebsite/engine/tpl.php new file mode 100644 index 0000000..a12807d --- /dev/null +++ b/localwebsite/engine/tpl.php @@ -0,0 +1,520 @@ +<?php + +abstract class base_tpl { + + public $twig; + protected $vars = []; + protected $global_vars = []; + protected $title = ''; + protected $title_modifiers = []; + protected $keywords = ''; + protected $description = ''; + protected $js = []; + protected $lang_keys = []; + protected $static = []; + protected $external_static = []; + protected $head = []; + protected $globals_applied = false; + protected $static_time; + + public function __construct($templates_dir, $cache_dir) { + global $config; + + $cl = get_called_class(); + + $this->twig = self::twig_instance($templates_dir, $cache_dir, $config['is_dev']); + $this->static_time = time(); + } + + public static function twig_instance($templates_dir, $cache_dir, $auto_reload) { + // must specify a second argument ($rootPath) here + // otherwise it will be getcwd() and it's www-prod/htdocs/ for apache and www-prod/ for cli code + // this is bad for templates rebuilding + $twig_loader = new \Twig\Loader\FilesystemLoader($templates_dir, ROOT); + + $env_options = []; + if (!is_null($cache_dir)) { + $env_options += [ + 'cache' => $cache_dir, + 'auto_reload' => $auto_reload + ]; + } + + $twig = new \Twig\Environment($twig_loader, $env_options); + $twig->addExtension(new Twig_MyExtension); + + return $twig; + } + + public function render($template, array $vars = []) { + $this->apply_globals(); + return $this->do_render($template, array_merge($this->vars, $vars)); + } + + protected function do_render($template, $vars) { + global $config; + $s = ''; + try { + $s = $this->twig->render($template, $vars); + } catch (\Twig\Error\Error $e) { + $error = get_class($e).": failed to render"; + $source_ctx = $e->getSourceContext(); + if ($source_ctx) { + $path = $source_ctx->getPath(); + if (startsWith($path, ROOT)) + $path = substr($path, strlen(ROOT)+1); + $error .= " ".$source_ctx->getName()." (".$path.") at line ".$e->getTemplateLine(); + } + $error .= ": "; + $error .= $e->getMessage(); + debugError($error); + if ($config['is_dev']) + $s = $error."\n"; + } + return $s; + } + + public function set($arg1, $arg2 = null) { + if (is_array($arg1)) { + foreach ($arg1 as $key => $value) { + $this->vars[$key] = $value; + } + } elseif ($arg2 !== null) { + $this->vars[$arg1] = $arg2; + } + } + + public function is_set($key): bool { + return isset($this->vars[$key]); + } + + public function set_global($arg1, $arg2 = null) { + if (is_array($arg1)) { + foreach ($arg1 as $key => $value) { + $this->global_vars[$key] = $value; + } + } elseif ($arg2 !== null) { + $this->global_vars[$arg1] = $arg2; + } + } + + public function is_global_set($key): bool { + return isset($this->global_vars[$key]); + } + + public function get_global($key) { + return $this->is_global_set($key) ? $this->global_vars[$key] : null; + } + + public function apply_globals() { + if (!empty($this->global_vars) && !$this->globals_applied) { + foreach ($this->global_vars as $key => $value) + $this->twig->addGlobal($key, $value); + $this->globals_applied = true; + } + } + + /** + * @param string $title + */ + public function set_title($title) { + $this->title = $title; + } + + /** + * @return string + */ + public function get_title() { + $title = $this->title != '' ? $this->title : 'Домашний сайт'; + if (!empty($this->title_modifiers)) { + foreach ($this->title_modifiers as $modifier) { + $title = $modifier($title); + } + } + return $title; + } + + /** + * @param callable $callable + */ + public function add_page_title_modifier(callable $callable) { + if (!is_callable($callable)) { + trigger_error(__METHOD__.': argument is not callable'); + } else { + $this->title_modifiers[] = $callable; + } + } + + /** + * @param string $css_name + * @param null $extra + */ + public function add_static(string $name, $extra = null) { + global $config; + // $is_css = endsWith($name, '.css'); + $this->static[] = [$name, $extra]; + } + + public function add_external_static($type, $url) { + $this->external_static[] = ['type' => $type, 'url' => $url]; + } + + public function add_js($js) { + $this->js[] = $js; + } + + public function add_lang_keys(array $keys) { + $this->lang_keys = array_merge($this->lang_keys, $keys); + } + + public function add_head($html) { + $this->head[] = $html; + } + + public function get_head_html() { + global $config; + $lines = []; + $public_path = $config['static_public_path']; + foreach ($this->static as $val) { + list($name, $extra) = $val; + if (endsWith($name, '.js')) + $lines[] = self::js_link($public_path.'/'.$name, $config['static'][$name] ?? 1); + else + $lines[] = self::css_link($public_path.'/'.$name, $config['static'][$name] ?? 1, $extra); + } + if (!empty($this->external_static)) { + foreach ($this->external_static as $ext) { + if ($ext['type'] == 'js') + $lines[] = self::js_link($ext['url']); + else if ($ext['type'] == 'css') + $lines[] = self::css_link($ext['url']); + } + } + if (!empty($this->head)) { + $lines = array_merge($lines, $this->head); + } + return implode("\n", $lines); + } + + public static function js_link($name, $version = null): string { + if ($version !== null) + $name .= '?'.$version; + return '<script src="'.$name.'" type="text/javascript"></script>'; + } + + public static function css_link($name, $version = null, $extra = null) { + if ($version !== null) + $name .= '?'.$version; + $s = '<link'; + if (is_array($extra)) { + if (!empty($extra['id'])) + $s .= ' id="'.$extra['id'].'"'; + } + $s .= ' rel="stylesheet" type="text/css"'; + if (is_array($extra) && !empty($extra['media'])) + $s .= ' media="'.$extra['media'].'"'; + $s .= ' href="'.$name.'"'; + $s .= '>'; + return $s; + } + + public function get_lang_keys() { + global $lang; + $keys = []; + if (!empty($this->lang_keys)) { + foreach ($this->lang_keys as $key) + $keys[$key] = $lang[$key]; + } + return $keys; + } + + public function render_not_found() { + http_response_code(404); + if (!is_xhr_request()) { + $this->render_page('404.twig'); + } else { + ajax_error(['code' => 404]); + } + } + + /** + * @param null|string $reason + */ + public function render_forbidden($reason = null) { + http_response_code(403); + if (!is_xhr_request()) { + $this->set(['reason' => $reason]); + $this->render_page('403.twig'); + } else { + $data = ['code' => 403]; + if (!is_null($reason)) + $data['reason'] = $reason; + ajax_error($data); + } + } + + public function must_revalidate() { + header('Cache-Control: no-store, no-cache, must-revalidate'); + } + + abstract public function render_page($template); + +} + +class web_tpl extends base_tpl { + + protected $alternate = false; + + public function __construct() { + global $config; + $templates = $config['templates']['web']; + parent::__construct( + ROOT.'/'. $templates['root'], + $config['twig_cache'] + ? ROOT.'/'.$templates['cache'] + : null + ); + } + + public function set_alternate($alt) { + $this->alternate = $alt; + } + + public function render_page($template) { + echo $this->_render_header(); + echo $this->_render_body($template); + echo $this->_render_footer(); + exit; + } + + public function _render_header() { + global $config; + $this->apply_globals(); + + $vars = [ + 'title' => $this->get_title(), + 'keywords' => $this->keywords, + 'description' => $this->description, + 'alternate' => $this->alternate, + 'static' => $this->get_head_html(), + ]; + return $this->do_render('header.twig', $vars); + } + + public function _render_body($template) { + return $this->do_render($template, $this->vars); + } + + public function _render_footer() { + $exec_time = microtime(true) - START_TIME; + $exec_time = round($exec_time, 4); + + $footer_vars = [ + 'exec_time' => $exec_time, + 'js' => !empty($this->js) ? implode("\n", $this->js) : '', + ]; + return $this->do_render('footer.twig', $footer_vars); + } + +} + +class Twig_MyExtension extends \Twig\Extension\AbstractExtension { + + public function getFilters() { + global $lang; + + return array( + new \Twig\TwigFilter('lang', 'lang'), + + new \Twig\TwigFilter('lang', function($key, array $args = []) use (&$lang) { + array_walk($args, function(&$item, $key) { + $item = htmlescape($item); + }); + array_unshift($args, $key); + return call_user_func_array([$lang, 'get'], $args); + }, ['is_variadic' => true]), + + new \Twig\TwigFilter('plural', function($text, array $args = []) use (&$lang) { + array_unshift($args, $text); + return call_user_func_array([$lang, 'num'], $args); + }, ['is_variadic' => true]), + + new \Twig\TwigFilter('format_number', function($number, array $args = []) { + array_unshift($args, $number); + return call_user_func_array('formatNumber', $args); + }, ['is_variadic' => true]), + + new \Twig\TwigFilter('short_number', function($number, array $args = []) { + array_unshift($args, $number); + return call_user_func_array('shortNumber', $args); + }, ['is_variadic']), + + new \Twig\TwigFilter('format_time', function($ts, array $args = []) { + array_unshift($args, $ts); + return call_user_func_array('formatTime', $args); + }, ['is_variadic' => true]), + + new \Twig\TwigFilter('format_duration', function($seconds, array $args = []) { + array_unshift($args, $seconds); + return call_user_func_array('formatDuration', $args); + }, ['is_variadic' => true]), + ); + } + + public function getTokenParsers() { + return [new JsTagTokenParser()]; + } + + public function getName() { + return 'lang'; + } + +} + +// Based on https://stackoverflow.com/questions/26170727/how-to-create-a-twig-custom-tag-that-executes-a-callback +class JsTagTokenParser extends \Twig\TokenParser\AbstractTokenParser { + + public function parse(\Twig\Token $token) { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + // recovers all inline parameters close to your tag name + $params = array_merge([], $this->getInlineParams($token)); + + $continue = true; + while ($continue) { + // create subtree until the decideJsTagFork() callback returns true + $body = $this->parser->subparse(array ($this, 'decideJsTagFork')); + + // I like to put a switch here, in case you need to add middle tags, such + // as: {% js %}, {% nextjs %}, {% endjs %}. + $tag = $stream->next()->getValue(); + switch ($tag) { + case 'endjs': + $continue = false; + break; + default: + throw new \Twig\Error\SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "endjs" to close the "mytag" block started at line %d)', $lineno), -1); + } + + // you want $body at the beginning of your arguments + array_unshift($params, $body); + + // if your endjs can also contains params, you can uncomment this line: + // $params = array_merge($params, $this->getInlineParams($token)); + // and comment this one: + $stream->expect(\Twig\Token::BLOCK_END_TYPE); + } + + return new JsTagNode(new \Twig\Node\Node($params), $lineno, $this->getTag()); + } + + /** + * Recovers all tag parameters until we find a BLOCK_END_TYPE ( %} ) + * + * @param \Twig\Token $token + * @return array + */ + protected function getInlineParams(\Twig\Token $token) { + $stream = $this->parser->getStream(); + $params = array (); + while (!$stream->test(\Twig\Token::BLOCK_END_TYPE)) { + $params[] = $this->parser->getExpressionParser()->parseExpression(); + } + $stream->expect(\Twig\Token::BLOCK_END_TYPE); + return $params; + } + + /** + * Callback called at each tag name when subparsing, must return + * true when the expected end tag is reached. + * + * @param \Twig\Token $token + * @return bool + */ + public function decideJsTagFork(\Twig\Token $token) { + return $token->test(['endjs']); + } + + /** + * Your tag name: if the parsed tag match the one you put here, your parse() + * method will be called. + * + * @return string + */ + public function getTag() { + return 'js'; + } + +} + +class JsTagNode extends \Twig\Node\Node { + + public function __construct($params, $lineno = 0, $tag = null) { + parent::__construct(['params' => $params], [], $lineno, $tag); + } + + public function compile(\Twig\Compiler $compiler) { + $count = count($this->getNode('params')); + + $compiler->addDebugInfo($this); + $compiler + ->write('global $__tpl;') + ->raw(PHP_EOL); + + for ($i = 0; ($i < $count); $i++) { + // argument is not an expression (such as, a \Twig\Node\Textbody) + // we should trick with output buffering to get a valid argument to pass + // to the functionToCall() function. + if (!($this->getNode('params')->getNode($i) instanceof \Twig\Node\Expression\AbstractExpression)) { + $compiler + ->write('ob_start();') + ->raw(PHP_EOL); + + $compiler + ->subcompile($this->getNode('params')->getNode($i)); + + $compiler + ->write('$js = ob_get_clean();') + ->raw(PHP_EOL); + } + } + + $compiler + ->write('$__tpl->add_js($js);') + ->raw(PHP_EOL) + ->write('unset($js);') + ->raw(PHP_EOL); + } + +} + + + +/** + * @param $data + */ +function ajax_ok($data) { + ajax_response(['response' => $data]); +} + +/** + * @param $error + * @param int $code + */ +function ajax_error($error, $code = 200) { + ajax_response(['error' => $error], $code); +} + +/** + * @param $data + * @param int $code + */ +function ajax_response($data, $code = 200) { + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); + header('Content-Type: application/json; charset=utf-8'); + http_response_code($code); + echo jsonEncode($data); + exit; +}
\ No newline at end of file |