false, 'wide' => false, 'logo_path_map' => [], 'logo_link_map' => [], 'is_index' => false, 'head_section' => null, 'articles_lang' => null, ]; public array $static = []; public array $svg_defs = []; }; function render($f, ...$vars): void { global $SkinState, $config; add_skin_strings(['4in1']); $ctx = skin(substr($f, 0, ($pos = strrpos(str_replace('/', '\\', $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 = skin('base'); $lang = []; foreach ($SkinState->lang as $key) $lang[$key] = lang($key); $lang = !empty($lang) ? jsonEncode($lang) : ''; $title = $SkinState->title; if (!$SkinState->options['is_index']) $title = lang('4in1').' - '.$title; $html = $layout_ctx->layout( static: $SkinState->static, theme: $theme, title: $title, opts: $SkinState->options, js: $js, meta: $SkinState->meta, unsafe_lang: $lang, unsafe_body: $body, exec_time: exectime(), admin_email: $config['admin_email'], svg_defs: $SkinState->svg_defs ); 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; static $twitter_limits = [ 'title' => 70, 'description' => 200 ]; $real_meta = []; $add_og_twitter = function($key, $value) use (&$real_meta, $twitter_limits) { foreach (['og', 'twitter'] as $social) { if ($social == 'twitter' && isset($twitter_limits[$key])) { if (mb_strlen($value) > $twitter_limits[$key]) $value = mb_substr($value, 0, $twitter_limits[$key]-3).'...'; } $real_meta[] = [ $social == 'twitter' ? 'name' : 'property' => $social.':'.$key, 'content' => $value ]; } }; foreach ($data as $key => $value) { if (str_starts_with($value, '$')) $value = lang(substr($value, 1)); switch ($key) { case '$url': case '$title': case '$image': $add_og_twitter(substr($key, 1), $value); break; case '$description': $add_og_twitter(substr($key, 1), $value); $real_meta[] = ['name' => 'description', 'content' => $value]; break; default: if (str_starts_with($key, 'og:')) { $real_meta[] = ['property' => $key, 'content' => $value]; } else { logWarning("unsupported meta: $key => $value"); } break; } } $SkinState->meta = array_merge($SkinState->meta, $real_meta); } class SkinContext { protected string $ns; protected array $data = []; function __construct(string $namespace) { $this->ns = $namespace; require_once APP_ROOT.str_replace('\\', DIRECTORY_SEPARATOR, $namespace).'.phps'; } 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 ($plain_args && !isset($arguments[$key])) break; 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, 'nl2br' => SkinStringModificationType::NL2BR, 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); } function bc(array $items, ?string $style = null, bool $mt = false): string { static $chevron = ''; $buf = implode(array_map(function(array $i) use ($chevron): string { $buf = ''; $has_url = array_key_exists('url', $i); if ($has_url) $buf .= ''; else $buf .= ''; $buf .= htmlescape($i['text']); if ($has_url) $buf .= ' '.$chevron.''; else $buf .= ''; return $buf; }, $items)); $class = 'bc'; if ($mt) $class .= ' mt'; return '