'modern', // Available themes with their display names 'available_themes' => [ 'default' => 'Default built-in theme', 'modern' => 'Modern theme', 'retro' => 'Alternative retro theme' ], // Path configurations 'paths' => [ // Base directory for all external themes 'themes' => __DIR__ . '/../../themes', // Default templates location (built-in fallback) 'templates' => __DIR__ . '/../templates', // Public assets directory (built-in fallback) 'public' => __DIR__ . '/../../public_html' ], // Theme configuration defaults 'default_config' => [ 'name' => 'Unnamed Theme', 'description' => 'A Jilo Web theme', 'version' => '1.0.0', 'author' => 'Lindeas Inc.', 'screenshot' => 'screenshot.png', 'options' => [] ] ]; EOT; file_put_contents($configFile, $configContent); } // Load the configuration self::$config = require $configFile; return self::$config; } /** * @var string Current theme name */ private static $currentTheme; /** * Initialize the theme system */ public static function init() { // Only load config if not already loaded if (self::$config === null) { try { self::getConfig(); // This will create default config if needed } catch (Exception $e) { error_log('Failed to load theme configuration: ' . $e->getMessage()); // Fallback to default configuration self::$config = [ 'active_theme' => 'modern', 'available_themes' => [ 'modern' => ['name' => 'Modern'], 'retro' => ['name' => 'Retro'] ] ]; } } self::$currentTheme = self::getCurrentThemeName(); } /** * Get the current theme name * * @return string */ public static function getCurrentThemeName() { // Ensure session is started if (session_status() === PHP_SESSION_NONE) { Session::startSession(); } // Check if already determined if (self::$currentTheme !== null) { return self::$currentTheme; } // Try to get from session first $sessionTheme = isset($_SESSION['theme']) ? $_SESSION['theme'] : null; if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) { self::$currentTheme = $sessionTheme; } else { // Attempt to load per-user theme from DB if user is logged in and userObject is available if (Session::isValidSession() && isset($_SESSION['user_id']) && isset($GLOBALS['userObject']) && is_object($GLOBALS['userObject']) && method_exists($GLOBALS['userObject'], 'getUserTheme')) { try { $dbTheme = $GLOBALS['userObject']->getUserTheme((int)$_SESSION['user_id']); if ($dbTheme && isset(self::$config['available_themes'][$dbTheme]) && self::themeExists($dbTheme)) { // Set session and current theme to the user's stored preference $_SESSION['theme'] = $dbTheme; self::$currentTheme = $dbTheme; } } catch (\Throwable $e) { // Ignore and continue to default fallback } } // Fall back to default theme if still not determined if (self::$currentTheme === null) { self::$currentTheme = self::$config['active_theme']; } } return self::$currentTheme; } /** * Get the URL for a theme asset * * @param string $themeId Theme ID * @param string $assetPath Path to the asset relative to theme directory (e.g., 'css/style.css') * @return string|null URL to the asset or null if not found */ public static function getAssetUrl($themeId, $assetPath = '') { // Clean and validate the asset path $assetPath = ltrim($assetPath, '/'); if (empty($assetPath)) { return null; } // Only allow alphanumeric, hyphen, underscore, dot, and forward slash if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) { return null; } // Prevent directory traversal if (strpos($assetPath, '..') !== false) { return null; } $fullPath = __DIR__ . "/../../themes/$themeId/$assetPath"; if (!file_exists($fullPath) || !is_readable($fullPath)) { return null; } // Generate URL that goes through index.php global $app_root; // Remove any trailing slash from app_root to avoid double slashes $baseUrl = rtrim($app_root, '/'); return "$baseUrl/?page=theme-asset&theme=" . urlencode($themeId) . "&path=" . urlencode($assetPath); } /** * Set the current theme for the session * * @param string $themeName * @return bool */ public static function setCurrentTheme(string $themeName, bool $persist = true): bool { if (!self::themeExists($themeName)) { return false; } // Update session if (Session::isValidSession()) { $_SESSION['theme'] = $themeName; } else { return false; } // Clear the current theme cache self::$currentTheme = null; // Persist per-user preference in DB when available and requested if ($persist && Session::isValidSession() && isset($_SESSION['user_id'])) { // Try to use existing user object if available if (isset($GLOBALS['userObject']) && is_object($GLOBALS['userObject']) && method_exists($GLOBALS['userObject'], 'setUserTheme')) { try { $GLOBALS['userObject']->setUserTheme((int)$_SESSION['user_id'], $themeName); } catch (\Throwable $e) { // Non-fatal: keep session theme even if DB save fails error_log('Failed to persist user theme: ' . $e->getMessage()); } } } self::$currentTheme = $themeName; return true; } /** * Check if a theme exists * * @param string $themeName * @return bool */ public static function themeExists(string $themeName): bool { // Default theme always exists as it uses core templates if ($themeName === 'default') { return true; } $themePath = self::getThemePath($themeName); return is_dir($themePath) && file_exists("$themePath/config.php"); } /** * Get the path to a theme * * @param string|null $themeName * @return string */ public static function getThemePath(?string $themeName = null): string { $themeName = $themeName ?? self::getCurrentThemeName(); $config = self::getConfig(); return rtrim($config['paths']['themes'], '/') . "/$themeName"; } /** * Get the URL for a theme asset * * @param string $path * @param bool $includeVersion * @return string */ public static function asset($path, $includeVersion = false) { $themeName = self::getCurrentThemeName(); $config = self::getConfig(); $baseUrl = rtrim($GLOBALS['app_root'] ?? '', '/'); // For non-default themes, use theme assets if ($themeName !== 'default') { $assetPath = "/themes/{$themeName}/assets/" . ltrim($path, '/'); // Add version query string for cache busting if ($includeVersion) { $version = self::getThemeVersion($themeName); $assetPath .= (strpos($assetPath, '?') !== false ? '&' : '?') . 'v=' . $version; } } else { // For default theme, use public_html directly $assetPath = '/' . ltrim($path, '/'); } return $baseUrl . $assetPath; } /** * Include a theme template file * * @param string $template Template name without .php extension * @return void */ public static function include($template) { global $config; $config = $config ?? []; $themeConfig = self::getConfig(); $themeName = self::getCurrentThemeName(); // We need this here, otherwise because this helper // between index and the views breaks the session vars extract($GLOBALS, EXTR_SKIP | EXTR_REFS); // Ensure config is always available in templates $config = array_merge($config, $themeConfig); // For non-default themes, look in the theme directory first if ($themeName !== 'default') { $themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php'; if (file_exists($themePath)) { include $themePath; return; } } // Fallback to default template location $defaultPath = $config['paths']['templates'] . '/' . $template . '.php'; if (file_exists($defaultPath)) { include $defaultPath; return; } // Log error if template not found error_log("Template not found: {$template} in theme: {$themeName}"); } /** * Get all available themes * * @return array */ public static function getAvailableThemes(): array { $config = self::getConfig(); $availableThemes = $config['available_themes'] ?? []; $themes = []; // Add default theme if not already present if (!isset($availableThemes['default'])) { $availableThemes['default'] = 'Default built-in theme'; } // Verify each theme exists and has a config file $themesDir = $config['paths']['themes'] ?? (__DIR__ . '/../../themes'); foreach ($availableThemes as $id => $name) { if ($id === 'default' || (is_dir("$themesDir/$id") && file_exists("$themesDir/$id/config.php"))) { $themes[$id] = $name; } } return $themes; } } // Initialize the theme system Theme::init();