summaryrefslogtreecommitdiff
path: root/localwebsite/engine
diff options
context:
space:
mode:
Diffstat (limited to 'localwebsite/engine')
-rw-r--r--localwebsite/engine/debug.php355
-rw-r--r--localwebsite/engine/request_handler.php142
-rw-r--r--localwebsite/engine/router.php199
-rw-r--r--localwebsite/engine/tpl.php520
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