v3.0.0.0
FlatlyPage
- <?php
- define('BASE_DIR', __DIR__);
-
- function flatly_detect_base_url(): string
- {
- $override = __DIR__ . '/data/base-url.txt';
- if (is_file($override)) {
- $base = trim((string) @file_get_contents($override));
- } else {
- $root = str_replace('\\', '/', (string) realpath(__DIR__));
- $scriptFile = str_replace('\\', '/', (string) realpath($_SERVER['SCRIPT_FILENAME'] ?? ''));
- $scriptName = str_replace('\\', '/', (string) ($_SERVER['SCRIPT_NAME'] ?? ''));
- $base = null;
-
- if ($root !== '' && $scriptFile !== '' && stripos($scriptFile, $root . '/') === 0) {
- $relative = substr($scriptFile, strlen($root));
- $length = strlen($relative);
- if (strlen($scriptName) >= $length && strcasecmp(substr($scriptName, -$length), $relative) === 0) {
- $base = substr($scriptName, 0, strlen($scriptName) - $length);
- }
- }
-
- if ($base === null) {
- $docRoot = str_replace('\\', '/', (string) realpath($_SERVER['DOCUMENT_ROOT'] ?? ''));
- $base = ($docRoot !== '' && stripos($root, $docRoot) === 0) ? substr($root, strlen($docRoot)) : '';
- }
- }
-
- $base = '/' . trim((string) $base, '/');
- if ($base === '/' || !preg_match('#^(/[A-Za-z0-9._~\-]+)+$#', $base)) {
- return '';
- }
- return $base;
- }
-
- define('BASE_URL', flatly_detect_base_url());
-
- function flatly_rewrite_urls(string $html): string
- {
- if (BASE_URL === '' || $html === '') {
- return $html;
- }
-
- $base = BASE_URL;
- $prefix = static function (string $path) use ($base): string {
- if ($path === '' || $path[0] !== '/' || (isset($path[1]) && $path[1] === '/')) {
- return $path;
- }
- if ($path === $base) {
- return $path;
- }
- foreach (['/', '?', '#'] as $next) {
- if (strpos($path, $base . $next) === 0) {
- return $path;
- }
- }
- return $base . $path;
- };
-
- $html = preg_replace_callback(
- '/(\s(?:href|src|action|poster|formaction|data-src)\s*=\s*)(["\'])(\/(?!\/)[^"\']*)\2/i',
- static fn(array $m): string => $m[1] . $m[2] . $prefix($m[3]) . $m[2],
- $html
- ) ?? $html;
-
- $html = preg_replace_callback(
- '/(\ssrcset\s*=\s*)(["\'])([^"\']*)\2/i',
- static function (array $m) use ($prefix): string {
- $parts = array_map(static function (string $candidate) use ($prefix): string {
- $candidate = trim($candidate);
- $pieces = preg_split('/\s+/', $candidate, 2);
- $pieces[0] = $prefix($pieces[0]);
- return implode(' ', $pieces);
- }, explode(',', $m[3]));
- return $m[1] . $m[2] . implode(', ', $parts) . $m[2];
- },
- $html
- ) ?? $html;
-
- $html = preg_replace_callback(
- '/url\(\s*(["\']?)(\/(?!\/)[^)"\']*)\1\s*\)/i',
- static fn(array $m): string => 'url(' . $m[1] . $prefix($m[2]) . $m[1] . ')',
- $html
- ) ?? $html;
-
- $script = '<script>window.FLATLY_BASE=' . json_encode($base, JSON_UNESCAPED_SLASHES) . ';</script>';
- $injected = preg_replace('/<head(\s[^>]*)?>/i', '$0' . $script, $html, 1);
- return $injected ?? $html;
- }
-
- if (session_status() === PHP_SESSION_NONE) {
- if (BASE_URL !== '') {
- session_name('FLATLY' . substr(md5(BASE_DIR), 0, 10));
- session_set_cookie_params(['path' => BASE_URL . '/', 'httponly' => true, 'samesite' => 'Lax']);
- }
- session_start();
- }
-
- if (BASE_URL !== '' && PHP_SAPI !== 'cli') {
- ob_start(static function (string $buffer): string {
- foreach (headers_list() as $header) {
- if (stripos($header, 'content-type:') === 0 && stripos($header, 'html') === false) {
- return $buffer;
- }
- }
- return flatly_rewrite_urls($buffer);
- });
- }
-
- $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 ? 'https://' : 'http://';
- $host = $_SERVER['HTTP_HOST'];
-
- function flatly_configured_site_url(): ?string
- {
- $file = __DIR__ . '/data/sitemap-config.php';
- $config = is_file($file) ? include $file : null;
- $value = is_array($config) ? trim((string) ($config['website_domain'] ?? '')) : '';
- if ($value === '') {
- return null;
- }
- if (!preg_match('#^https?://#i', $value)) {
- $value = 'https://' . $value;
- }
- $parts = parse_url($value);
- if (empty($parts['host'])) {
- return null;
- }
- $url = strtolower($parts['scheme']) . '://' . strtolower($parts['host']) . (isset($parts['port']) ? ':' . $parts['port'] : '');
- $path = rtrim($parts['path'] ?? '', '/');
- return $url . ($path !== '' ? $path : BASE_URL);
- }
-
- $configured_site_url = flatly_configured_site_url();
-
- define('SITE_NAME', 'FlatlyPage');
- define('SITE_URL', $configured_site_url ?? $protocol . $host . BASE_URL);
- define('DATA_DIR', __DIR__ . '/data/');
-
- if ($configured_site_url !== null && !headers_sent()
- && strcasecmp((string) preg_replace('/:\d+$/', '', $host), (string) parse_url($configured_site_url, PHP_URL_HOST)) !== 0) {
- header('X-Robots-Tag: noindex, nofollow');
- }
- define('ADMIN_DIR', __DIR__ . '/admin/');
-
- require_once __DIR__ . '/languages.php';
-
- define('CSRF_TOKEN_NAME', 'csrf_token');
-
- if (!is_dir(DATA_DIR)) {
- mkdir(DATA_DIR, 0755, true);
- }
-
- function generate_csrf_token(): string
- {
- if (empty($_SESSION[CSRF_TOKEN_NAME])) {
- $_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
- }
- return $_SESSION[CSRF_TOKEN_NAME];
- }
-
- function verify_csrf_token(string $token): bool
- {
- return isset($_SESSION[CSRF_TOKEN_NAME]) && hash_equals($_SESSION[CSRF_TOKEN_NAME], $token);
- }
-
- function is_logged_in(): bool
- {
- return isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true;
- }
-
- function require_login(): void
- {
- if (!is_logged_in()) {
- header('Location: ' . BASE_URL . '/admin');
- exit;
- }
- if (!headers_sent()) {
- header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
- header('Pragma: no-cache');
- }
- check_ip_ban();
- }
-
- function check_ip_ban(): void
- {
- static $tracker = null;
-
- if ($tracker === null) {
- require_once __DIR__ . '/admin/login_tracking.php';
- $tracker = new LoginTracker();
- }
-
- $clientIP = $tracker->getClientIP();
-
- if ($tracker->isIPBanned($clientIP)) {
- session_unset();
- session_destroy();
-
- if (isset($_COOKIE[session_name()])) {
- setcookie(session_name(), '', time() - 3600, session_get_cookie_params()['path']);
- }
-
- http_response_code(403);
- exit('Access forbidden.');
- }
- }
-
- function e(string $string): string
- {
- return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
- }
-
- function slugify(string $text): string
- {
- $text = preg_replace('~[^\pL\d]+~u', '-', $text);
- $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
- $text = preg_replace('~[^-\w]+~', '', $text);
- $text = trim($text, '-');
- $text = preg_replace('~-+~', '-', $text);
- $text = strtolower($text);
- return empty($text) ? 'n-a' : $text;
- }
-
- function media_types(): array
- {
- return [
- 'image' => [
- 'jpg' => ['image/jpeg'],
- 'jpeg' => ['image/jpeg'],
- 'png' => ['image/png'],
- 'gif' => ['image/gif'],
- 'webp' => ['image/webp'],
- 'avif' => ['image/avif'],
- 'ico' => ['image/x-icon', 'image/vnd.microsoft.icon'],
- ],
- 'video' => [
- 'mp4' => ['video/mp4'],
- 'm4v' => ['video/mp4', 'video/x-m4v'],
- 'webm' => ['video/webm'],
- 'ogv' => ['video/ogg', 'application/ogg'],
- 'mov' => ['video/quicktime', 'video/mp4'],
- ],
- 'audio' => [
- 'mp3' => ['audio/mpeg', 'audio/mp3'],
- 'ogg' => ['audio/ogg', 'application/ogg'],
- 'oga' => ['audio/ogg', 'application/ogg'],
- 'opus' => ['audio/ogg', 'audio/opus', 'application/ogg'],
- 'wav' => ['audio/wav', 'audio/x-wav', 'audio/vnd.wave'],
- 'm4a' => ['audio/mp4', 'audio/x-m4a', 'video/mp4'],
- 'aac' => ['audio/aac', 'audio/x-hx-aac-adts'],
- 'flac' => ['audio/flac', 'audio/x-flac'],
- 'weba' => ['audio/webm', 'video/webm'],
- ],
- ];
- }
-
- function media_kind_for_extension(string $ext): ?string
- {
- $ext = strtolower($ext);
- foreach (media_types() as $kind => $exts) {
- if (isset($exts[$ext])) {
- return $kind;
- }
- }
- return null;
- }
-
- function media_mime_for_url(string $url, string $fallback): string
- {
- $path = parse_url($url, PHP_URL_PATH) ?: $url;
- $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
- $map = [
- 'mp4' => 'video/mp4', 'm4v' => 'video/mp4', 'webm' => 'video/webm', 'ogv' => 'video/ogg', 'mov' => 'video/mp4',
- 'mp3' => 'audio/mpeg', 'ogg' => 'audio/ogg', 'oga' => 'audio/ogg', 'opus' => 'audio/ogg', 'wav' => 'audio/wav',
- 'm4a' => 'audio/mp4', 'aac' => 'audio/aac', 'flac' => 'audio/flac', 'weba' => 'audio/webm',
- ];
- return $map[$ext] ?? $fallback;
- }
-
- function invalidate_php_cache(string $filepath): void
- {
- clearstatcache(true, $filepath);
- if (function_exists('opcache_invalidate')) {
- @opcache_invalidate($filepath, true);
- }
- }
-
- function load_page(string $filename): ?array
- {
- foreach ([$filename . '.php', 'default-' . $filename . '.php'] as $candidate) {
- $filepath = DATA_DIR . $candidate;
- if (file_exists($filepath)) {
- return include $filepath;
- }
- }
- return null;
- }
-
- function save_page(string $filename, array $data): bool
- {
- unset($GLOBALS['_products_cache']);
- $filepath = DATA_DIR . $filename . '.php';
- $content = "<?php\nreturn " . var_export($data, true) . ";\n";
- $written = file_put_contents($filepath, $content) !== false;
- invalidate_php_cache($filepath);
- return $written;
- }
-
- function get_site_settings(?string $lang = null): array
- {
- $lang ??= flatly_current_lang();
- $settings = load_page('settings') ?? get_default_site_settings();
- if ($lang !== null) {
- $translation = load_page_lang('settings', $lang);
- if (is_array($translation)) {
- $settings = flatly_merge_translation($settings, $translation);
- }
- $settings['site_language'] = $lang;
- }
- return $settings;
- }
-
- function get_default_site_settings(): array
- {
- return [
- 'site_name' => SITE_NAME,
- 'site_description' => 'Build amazing websites with ease',
- 'logo_text' => SITE_NAME,
- 'site_language' => 'en',
- 'logo_image' => '',
- 'favicon' => '',
- 'primary_color' => '#ffffff',
- 'nav_links' => [
- ['label' => 'Features', 'url' => '#features'],
- ['label' => 'Testimonials', 'url' => '#testimonials'],
- ['label' => 'Pricing', 'url' => '#pricing'],
- ],
- 'nav_buttons' => [
- ['label' => 'Log in', 'url' => '#', 'style' => 'ghost'],
- ['label' => 'Get Started', 'url' => '#', 'style' => 'primary'],
- ],
- 'footer' => [
- 'brand_description' => 'A lightweight, self-hosted CMS that lets you create and manage websites with ease.',
- 'columns' => [
- [
- 'title' => 'Product',
- 'links' => [
- ['label' => 'Features', 'url' => '#features'],
- ['label' => 'Pricing', 'url' => '#pricing'],
- ['label' => 'Integrations', 'url' => '#'],
- ]
- ],
- [
- 'title' => 'Company',
- 'links' => [
- ['label' => 'About', 'url' => '#'],
- ['label' => 'Blog', 'url' => '#'],
- ['label' => 'Careers', 'url' => '#'],
- ]
- ],
- [
- 'title' => 'Resources',
- 'links' => [
- ['label' => 'Documentation', 'url' => '#'],
- ['label' => 'Guides', 'url' => '#'],
- ['label' => 'Support', 'url' => '#'],
- ]
- ],
- [
- 'title' => 'Legal',
- 'links' => [
- ['label' => 'Privacy', 'url' => '#'],
- ['label' => 'Terms', 'url' => '#'],
- ]
- ],
- ],
- 'social_links' => [
- ['platform' => 'twitter', 'url' => '#'],
- ['platform' => 'github', 'url' => '#'],
- ['platform' => 'linkedin', 'url' => '#'],
- ],
- 'copyright' => '© ' . date('Y') . ' ' . SITE_NAME . '. All rights reserved.',
- 'bottom_links' => [
- ['label' => 'Privacy Policy', 'url' => '#'],
- ['label' => 'Terms of Service', 'url' => '#'],
- ],
- ],
- ];
- }
-
- function get_available_fonts(): array
- {
- static $fonts = null;
- if ($fonts !== null) {
- return $fonts;
- }
- $fonts = [];
- foreach (glob(BASE_DIR . '/fonts/*.ttf') ?: [] as $file) {
- $base = pathinfo($file, PATHINFO_FILENAME);
- $label = trim(str_replace('_', ' ', $base));
- if ($label !== '' && preg_match('/^[\p{L}\p{N} \-.]+$/u', $label)) {
- $fonts[$label] = basename($file);
- }
- }
- ksort($fonts, SORT_NATURAL | SORT_FLAG_CASE);
- return $fonts;
- }
-
- function resolve_font(?string $name, string $default): string
- {
- $fonts = get_available_fonts();
- if ($name !== null && isset($fonts[$name])) {
- return $name;
- }
- if (isset($fonts[$default])) {
- return $default;
- }
- return (string) (array_key_first($fonts) ?? '');
- }
-
- function font_head_html(array $names, array $preload = []): string
- {
- $fonts = get_available_fonts();
- $css = '';
- $links = '';
- foreach (array_unique($names) as $name) {
- if (!isset($fonts[$name])) {
- continue;
- }
- $file = $fonts[$name];
- $url = '/fonts/' . rawurlencode($file) . '?v=' . (int) @filemtime(BASE_DIR . '/fonts/' . $file);
- $css .= "@font-face{font-family:'" . $name . "';font-style:normal;font-weight:100 900;font-display:swap;src:url('" . $url . "') format('truetype');}";
- if (in_array($name, $preload, true)) {
- $links .= '<link rel="preload" href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" as="font" type="font/ttf" crossorigin>' . "\n ";
- }
- }
- return $css === '' ? '' : $links . '<style>' . $css . '</style>';
- }
-
- function get_system_settings(): array
- {
- static $settings = null;
- if ($settings !== null) {
- return $settings;
- }
- $stored = load_page('system');
- $settings = array_merge([
- 'translator_enabled' => true,
- 'translator_provider' => 'mymemory',
- 'translator_api_key' => '',
- 'dashboard_font' => 'Space Grotesk',
- 'ai_enabled' => false,
- 'ai_provider' => 'gemini',
- 'ai_model' => '',
- 'ai_api_key' => '',
- 'custom_js' => '',
- 'custom_html' => [],
- 'analytics' => [],
- ], is_array($stored) ? $stored : []);
- if (is_array($stored) && !isset($stored['dashboard_font']) && isset($stored['interface_font'])) {
- $settings['dashboard_font'] = $stored['interface_font'];
- }
- return $settings;
- }
-
- const CUSTOM_CODE_MAX_LENGTH = 100000;
-
- /**
- * Reads a code field that the admin sends base64-encoded as "<field>_b64" (so hosting firewalls
- * don't block posts containing <script>); falls back to the plain field. Works for arrays too.
- */
- function flatly_posted_code(string $field): mixed
- {
- $decode = static function (mixed $value): string {
- $decoded = is_string($value) ? base64_decode($value, true) : false;
-
- return $decoded !== false && mb_check_encoding($decoded, 'UTF-8') ? $decoded : '';
- };
- if (isset($_POST[$field . '_b64'])) {
- $encoded = $_POST[$field . '_b64'];
-
- return is_array($encoded) ? array_map($decode, $encoded) : $decode($encoded);
- }
-
- return $_POST[$field] ?? null;
- }
-
- function flatly_clean_custom_code(mixed $raw): string
- {
- $code = str_replace(["\r\n", "\r"], "\n", is_string($raw) ? $raw : '');
- $code = (string) preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $code);
-
- return mb_substr(trim($code), 0, CUSTOM_CODE_MAX_LENGTH);
- }
-
- /**
- * Custom JavaScript from Settings > Custom, printed at the end of <body> on public pages.
- * Plain code is wrapped in a <script> tag; a snippet that already has its own <script> tags
- * (e.g. an analytics embed) is printed as-is.
- */
- function flatly_custom_js(): string
- {
- $code = (string) (get_system_settings()['custom_js'] ?? '');
- if (trim($code) === '') {
- return '';
- }
- if (stripos($code, '<script') !== false) {
- return "\n" . $code . "\n";
- }
-
- return "\n<script>\n" . str_ireplace('</script', '<\/script', $code) . "\n</script>\n";
- }
-
- const CUSTOM_HTML_LOCATIONS = [
- 'head' => 'Head (before </head>)',
- 'nav' => 'Navigation (inside the top bar, after the buttons)',
- 'body_start' => 'Body start (right after <body>)',
- 'body_end' => 'Body end (before </body>)',
- ];
- const CUSTOM_HTML_MAX_SNIPPETS = 20;
-
- /** Builds the Custom HTML list from the parallel custom_html_location[] / custom_html_code[] form fields. */
- function flatly_clean_custom_html(mixed $locations, mixed $codes): array
- {
- $locations = is_array($locations) ? array_values($locations) : [];
- $codes = is_array($codes) ? array_values($codes) : [];
- $snippets = [];
- foreach ($codes as $index => $code) {
- $location = is_string($locations[$index] ?? null) ? $locations[$index] : '';
- $code = flatly_clean_custom_code($code);
- if ($code === '' || !isset(CUSTOM_HTML_LOCATIONS[$location])) {
- continue;
- }
- $snippets[] = ['location' => $location, 'code' => $code];
- if (count($snippets) >= CUSTOM_HTML_MAX_SNIPPETS) {
- break;
- }
- }
-
- return $snippets;
- }
-
- /** Custom HTML from Settings > Custom (and analytics scripts) for one location on public pages, printed as-is. */
- function flatly_custom_html(string $location): string
- {
- $out = flatly_analytics_html($location);
- foreach ((array) (get_system_settings()['custom_html'] ?? []) as $snippet) {
- if (is_array($snippet) && ($snippet['location'] ?? '') === $location && is_string($snippet['code'] ?? null) && trim($snippet['code']) !== '') {
- $out .= "\n" . $snippet['code'] . "\n";
- }
- }
-
- return $out;
- }
-
- const ANALYTICS_PLACEMENTS = [
- 'head' => '<head>',
- 'body_start' => '<body> start',
- 'body_end' => '<body> end',
- ];
- const ANALYTICS_MAX_SCRIPTS = 30;
-
- function flatly_analytics_defaults(): array
- {
- return [
- 'consent_enabled' => false,
- 'cookie_icon' => false,
- 'banner_text' => 'We use cookies to analyse traffic and improve your experience. You can accept or reject optional cookies.',
- 'accept_label' => 'Accept',
- 'reject_label' => 'Reject',
- 'privacy_url' => '',
- 'scripts' => [],
- ];
- }
-
- function flatly_analytics_settings(): array
- {
- $stored = get_system_settings()['analytics'] ?? [];
-
- return array_merge(flatly_analytics_defaults(), is_array($stored) ? $stored : []);
- }
-
- function flatly_clean_analytics(array $raw): array
- {
- $defaults = flatly_analytics_defaults();
- $text = static function (mixed $value, int $limit, string $fallback): string {
- $value = is_string($value) ? trim((string) preg_replace('/[\x00-\x1F\x7F]/u', ' ', $value)) : '';
-
- return $value === '' ? $fallback : mb_substr($value, 0, $limit);
- };
-
- $privacy = is_string($raw['privacy_url'] ?? null) ? trim($raw['privacy_url']) : '';
- if ($privacy !== '' && preg_match('#^(https?://|/)[^\s<>"\']*$#i', $privacy) !== 1) {
- $privacy = '';
- }
-
- $scripts = [];
- foreach (array_slice(is_array($raw['scripts'] ?? null) ? array_values($raw['scripts']) : [], 0, ANALYTICS_MAX_SCRIPTS) as $script) {
- if (!is_array($script)) {
- continue;
- }
- $code = flatly_clean_custom_code($script['code'] ?? '');
- $name = $text($script['name'] ?? '', 100, '');
- if ($code === '' || $name === '') {
- continue;
- }
- $id = is_string($script['id'] ?? null) && preg_match('/^[a-z0-9]{6,32}$/', $script['id']) === 1 ? $script['id'] : bin2hex(random_bytes(6));
- $scripts[] = [
- 'id' => $id,
- 'name' => $name,
- 'code' => $code,
- 'placement' => is_string($script['placement'] ?? null) && isset(ANALYTICS_PLACEMENTS[$script['placement']]) ? $script['placement'] : 'head',
- 'consent' => !empty($script['consent']),
- 'active' => !empty($script['active']),
- ];
- }
-
- return [
- 'consent_enabled' => !empty($raw['consent_enabled']),
- 'cookie_icon' => !empty($raw['cookie_icon']),
- 'banner_text' => $text($raw['banner_text'] ?? '', 600, $defaults['banner_text']),
- 'accept_label' => $text($raw['accept_label'] ?? '', 40, $defaults['accept_label']),
- 'reject_label' => $text($raw['reject_label'] ?? '', 40, $defaults['reject_label']),
- 'privacy_url' => $privacy,
- 'scripts' => $scripts,
- ];
- }
-
- /**
- * Analytics scripts for one placement. With the consent manager on, scripts that require consent
- * are printed inside an inert <template>; assets/js/consent.js runs them after the visitor accepts.
- */
- function flatly_analytics_html(string $location): string
- {
- $analytics = flatly_analytics_settings();
- $gate = !empty($analytics['consent_enabled']);
- $out = '';
-
- foreach ((array) $analytics['scripts'] as $script) {
- if (!is_array($script) || empty($script['active']) || ($script['placement'] ?? '') !== $location || !is_string($script['code'] ?? null)) {
- continue;
- }
- $out .= $gate && !empty($script['consent'])
- ? "\n<template data-flatly-consent>\n" . $script['code'] . "\n</template>\n"
- : "\n" . $script['code'] . "\n";
- }
-
- if ($gate && $location === 'body_end') {
- $config = [
- 'text' => (string) $analytics['banner_text'],
- 'accept' => (string) $analytics['accept_label'],
- 'reject' => (string) $analytics['reject_label'],
- 'privacy' => (string) $analytics['privacy_url'],
- 'icon' => !empty($analytics['cookie_icon']),
- ];
- $version = @filemtime(BASE_DIR . '/assets/js/consent.js') ?: 1;
- $out .= "\n<script>window.FLATLY_CONSENT = " . json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP) . ";</script>\n"
- . '<script src="/assets/js/consent.js?v=' . $version . '"></script>' . "\n";
- }
-
- return $out;
- }
-
- function get_dashboard_font(): string
- {
- return resolve_font(get_system_settings()['dashboard_font'] ?? null, 'Space Grotesk');
- }
-
- function get_optional_font(array $site_settings, string $key): string
- {
- $name = $site_settings[$key] ?? '';
- return $name !== '' && isset(get_available_fonts()[$name]) ? $name : '';
- }
-
- function site_font_selectors(): array
- {
- return [
- 'headings_font' => 'h1,h2,h3,h4,h5,h6',
- 'logo_font' => '.logo',
- 'price_font' => '.pricing-card .price',
- ];
- }
-
- function site_extra_fonts(array $site_settings): array
- {
- $names = [];
- foreach (array_keys(site_font_selectors()) as $key) {
- $name = get_optional_font($site_settings, $key);
- if ($name !== '') {
- $names[] = $name;
- }
- }
- return array_values(array_unique($names));
- }
-
- function get_site_interface_font(array $site_settings): string
- {
- return resolve_font($site_settings['interface_font'] ?? null, 'Space Grotesk');
- }
-
- function site_all_fonts(array $site_settings): array
- {
- $names = array_merge([get_text_font($site_settings), get_site_interface_font($site_settings)], site_extra_fonts($site_settings));
- return array_values(array_unique(array_filter($names, fn($name) => $name !== '')));
- }
-
- function site_fonts_css(array $site_settings): string
- {
- $text = get_text_font($site_settings);
- $interface = get_site_interface_font($site_settings);
- $css = '';
- if ($interface !== '') {
- $stack = font_family_css($interface);
- $css .= 'body{font-family:' . $stack . ';}';
- $css .= 'main .btn,main button,main input,main select,main textarea{font-family:' . $stack . ';}';
- }
- if ($text !== '') {
- $css .= 'main{font-family:' . font_family_css($text) . ';}';
- }
- foreach (site_font_selectors() as $key => $selector) {
- $name = get_optional_font($site_settings, $key);
- if ($name !== '') {
- $css .= $selector . '{font-family:' . font_family_css($name) . ';}';
- }
- }
- return $css;
- }
-
- function get_headings_font(array $site_settings): string
- {
- $name = $site_settings['headings_font'] ?? '';
- return $name !== '' && isset(get_available_fonts()[$name]) ? $name : '';
- }
-
- function headings_font_css(string $name): string
- {
- return $name === '' ? '' : "h1,h2,h3,h4,h5,h6{font-family:" . font_family_css($name) . ";}";
- }
-
- function get_text_font(array $site_settings): string
- {
- return resolve_font($site_settings['website_font'] ?? null, 'EB Garamond');
- }
-
- function font_family_css(string $name, string $fallback = '-apple-system, BlinkMacSystemFont, sans-serif'): string
- {
- return $name === '' ? $fallback : "'" . $name . "', " . $fallback;
- }
-
- function admin_font_head(): string
- {
- $font = get_dashboard_font();
- $html = font_head_html([$font], [$font]);
- if ($font !== '') {
- $html .= '<style>:root{--font-interface:' . font_family_css($font, 'sans-serif') . ';}</style>';
- }
- return $html;
- }
-
- function nav_icon_html($icon): string
- {
- if (!is_string($icon) || !preg_match('/^icon-[a-z0-9-]+$/', $icon)) {
- return '';
- }
- return '<svg class="nav-icon" aria-hidden="true"><use href="/assets/icons.svg#' . $icon . '"></use></svg>';
- }
-
- function get_icon_ids(): array
- {
- static $ids = null;
- if ($ids !== null) {
- return $ids;
- }
- $ids = [];
- $file = BASE_DIR . '/assets/icons.svg';
- if (is_file($file) && preg_match_all('/<symbol[^>]*\bid="(icon-[a-z0-9-]+)"/', (string) file_get_contents($file), $m)) {
- $ids = array_values(array_unique($m[1]));
- }
- return $ids;
- }
-
- function theme_file_active(): bool
- {
- $path = BASE_DIR . '/css/theme.css';
- if (!is_file($path) || filesize($path) === 0) {
- return false;
- }
- $handle = fopen($path, 'r');
- $header = fread($handle, 25);
- fclose($handle);
- return trim($header) !== '/* No theme active */';
- }
-
- function theme_link_html(): string
- {
- if (!theme_file_active()) {
- return '';
- }
- return '<link rel="stylesheet" href="/css/theme.css?v=' . (int) filemtime(BASE_DIR . '/css/theme.css') . '">';
- }
-
- function get_all_products(?string $lang = null): array
- {
- $lang ??= flatly_current_lang();
- $cache_key = $lang ?? '';
- if (isset($GLOBALS['_products_cache'][$cache_key])) {
- return $GLOBALS['_products_cache'][$cache_key];
- }
-
- $products = [];
- $files = glob(DATA_DIR . ($lang !== null && flatly_lang_code_valid($lang) ? $lang . '-lang-' : '') . 'product-*.php') ?: [];
- foreach ($files as $file) {
- $data = include $file;
- if (is_array($data)) {
- $products[] = $data;
- }
- }
-
- usort($products, function ($a, $b) {
- return strtotime($b['created_at'] ?? '0') - strtotime($a['created_at'] ?? '0');
- });
- return $GLOBALS['_products_cache'][$cache_key] = $products;
- }
-
- function find_product_by_slug(string $slug, ?string $lang = null): ?array
- {
- $products = get_all_products($lang);
- foreach ($products as $product) {
- if (isset($product['slug']) && $product['slug'] === $slug) {
- return $product;
- }
- }
- return null;
- }
-
- function get_all_pages(?string $lang = null): array
- {
- $pages = [
- ['label' => 'Homepage', 'url' => flatly_lang_url($lang, '/')],
- ];
-
- $products = get_all_products($lang);
- foreach ($products as $product) {
- $pages[] = [
- 'label' => $product['title'] ?? 'Product',
- 'url' => flatly_lang_url($lang, '/' . ($product['slug'] ?? '')),
- ];
- }
-
- return $pages;
- }
-
- function getPages(): array
- {
- return get_all_products();
- }
-
- function getSiteSettings(): array
- {
- return get_site_settings();
- }
-
- function getNavigation(): array
- {
- $settings = get_site_settings();
- return [
- 'items' => $settings['nav_links'] ?? [],
- ];
- }
-
- function getFooterSettings(): array
- {
- $settings = get_site_settings();
- $footer = $settings['footer'] ?? [];
-
- return [
- 'description' => $footer['brand_description'] ?? 'A lightweight, self-hosted CMS that lets you create and manage websites with ease..',
- 'social_links' => $footer['social_links'] ?? [],
- 'columns' => $footer['columns'] ?? [],
- 'copyright' => $footer['copyright'] ?? '© ' . date('Y') . ' ' . SITE_NAME . '. All rights reserved.',
- 'bottom_links' => $footer['bottom_links'] ?? [],
- ];
- }
-
- function sanitize(string $input): string
- {
- return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
- }
-
- require_once BASE_DIR . '/engine/renderion.php';
-