false, 'wide' => false, 'dynlogo_enabled' => true, 'logo_path_map' => [], 'logo_link_map' => [], ]; public array $static = []; }; function render($f, ...$vars): void { global $SkinState; $f = '\\skin\\'.str_replace('/', '\\', $f); $ctx = new SkinContext(substr($f, 0, ($pos = strrpos($f, '\\')))); $body = call_user_func_array([$ctx, substr($f, $pos + 1)], $vars); if (is_array($body)) list($body, $js) = $body; else $js = null; $theme = getUserTheme(); if ($theme != 'auto' && !themeExists($theme)) $theme = 'auto'; $layout_ctx = new SkinContext('\\skin\\base'); $lang = []; foreach ($SkinState->lang as $key) $lang[$key] = lang($key); $lang = !empty($lang) ? json_encode($lang, JSON_UNESCAPED_UNICODE) : ''; $html = $layout_ctx->layout( static: $SkinState->static, theme: $theme, title: $SkinState->title, opts: $SkinState->options, js: $js, meta: $SkinState->meta, unsafe_lang: $lang, unsafe_body: $body, exec_time: exectime() ); echo $html; exit; } function set_title(string $title): void { global $SkinState; if (str_starts_with($title, '$')) $title = lang(substr($title, 1)); else if (str_starts_with($title, '\\$')) $title = substr($title, 1); $SkinState->title = $title; } function set_skin_opts(array $options) { global $SkinState; $SkinState->options = array_merge($SkinState->options, $options); } function add_skin_strings(array $keys): void { global $SkinState; $SkinState->lang = array_merge($SkinState->lang, $keys); } function add_skin_strings_re(string $re): void { global $__lang; add_skin_strings($__lang->search($re)); } function add_static(string ...$files): void { global $SkinState; foreach ($files as $file) $SkinState->static[] = $file; } function add_meta(array ...$data) { global $SkinState; $SkinState->meta = array_merge($SkinState->meta, $data); } class SkinContext { protected string $ns; protected array $data = []; function __construct(string $namespace) { $this->ns = $namespace; require_once APP_ROOT.str_replace('\\', DIRECTORY_SEPARATOR, $namespace).'.skin.php'; } function __call($name, array $arguments) { $plain_args = array_is_list($arguments); $fn = $this->ns.'\\'.$name; $refl = new ReflectionFunction($fn); $fparams = $refl->getParameters(); assert(count($fparams) == count($arguments) + 1, "$fn: invalid number of arguments (".count($fparams)." != ".(count($arguments) + 1).")"); foreach ($fparams as $n => $param) { if ($n == 0) continue; // skip $ctx $key = $plain_args ? $n - 1 : $param->name; if (!$plain_args && !array_key_exists($param->name, $arguments)) { if (!$param->isDefaultValueAvailable()) throw new InvalidArgumentException('argument '.$param->name.' not found'); else continue; } if (is_string($arguments[$key]) || $arguments[$key] instanceof SkinString) { if (is_string($arguments[$key])) $arguments[$key] = new SkinString($arguments[$key]); if (($pos = strpos($param->name, '_')) !== false) { $mod_type = match (substr($param->name, 0, $pos)) { 'unsafe' => SkinStringModificationType::RAW, 'urlencoded' => SkinStringModificationType::URL, 'jsonencoded' => SkinStringModificationType::JSON, 'addslashes' => SkinStringModificationType::ADDSLASHES, default => SkinStringModificationType::HTML }; } else { $mod_type = SkinStringModificationType::HTML; } $arguments[$key]->setModType($mod_type); } } array_unshift($arguments, $this); return call_user_func_array($fn, $arguments); } function &__get(string $name) { $fn = $this->ns.'\\'.$name; if (function_exists($fn)) { $f = [$this, $name]; return $f; } if (array_key_exists($name, $this->data)) return $this->data[$name]; } function __set(string $name, $value) { $this->data[$name] = $value; } function if_not($cond, $callback, ...$args) { return $this->_if_condition(!$cond, $callback, ...$args); } function if_true($cond, $callback, ...$args) { return $this->_if_condition($cond, $callback, ...$args); } function if_admin($callback, ...$args) { return $this->_if_condition(is_admin(), $callback, ...$args); } function if_dev($callback, ...$args) { return $this->_if_condition(is_dev(), $callback, ...$args); } function if_then_else($cond, $cb1, $cb2) { return $cond ? $this->_return_callback($cb1) : $this->_return_callback($cb2); } function csrf($key): string { return csrf_get($key); } protected function _if_condition($condition, $callback, ...$args) { if (is_string($condition) || $condition instanceof Stringable) $condition = (string)$condition !== ''; if ($condition) return $this->_return_callback($callback, $args); return ''; } protected function _return_callback($callback, $args = []) { if (is_callable($callback)) return call_user_func_array($callback, $args); else if (is_string($callback)) return $callback; } function for_each(array $iterable, callable $callback) { $html = ''; foreach ($iterable as $k => $v) $html .= call_user_func($callback, $v, $k); return $html; } function lang(...$args): string { return htmlescape($this->langRaw(...$args)); } function langRaw(string $key, ...$args) { $val = lang($key); return empty($args) ? $val : sprintf($val, ...$args); } } enum SkinStringModificationType { case RAW; case URL; case HTML; case JSON; case ADDSLASHES; } class SkinString implements Stringable { protected SkinStringModificationType $modType; function __construct(protected string $string) {} function setModType(SkinStringModificationType $modType) { $this->modType = $modType; } function __toString(): string { return match ($this->modType) { SkinStringModificationType::HTML => htmlescape($this->string), SkinStringModificationType::URL => urlencode($this->string), SkinStringModificationType::JSON => json_encode($this->string, JSON_UNESCAPED_UNICODE), SkinStringModificationType::ADDSLASHES => addslashes($this->string), default => $this->string, }; } }