2025-05-15 11:54:21 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Theme Helper
|
|
|
|
*
|
|
|
|
* Handles theme management and template/asset loading for the application.
|
|
|
|
* Supports multiple themes with fallback to default theme when needed.
|
|
|
|
* The default theme uses app/templates and public_html/static as fallbacks/
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace App\Helpers;
|
|
|
|
|
|
|
|
use Exception;
|
|
|
|
|
2025-05-23 06:58:50 +00:00
|
|
|
// Include Session class
|
|
|
|
require_once __DIR__ . '/../classes/session.php';
|
|
|
|
use Session;
|
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
class Theme
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var array Theme configuration
|
|
|
|
*/
|
|
|
|
private static $config;
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-23 06:58:50 +00:00
|
|
|
/**
|
|
|
|
* Get the theme configuration
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public static function getConfig()
|
|
|
|
{
|
2025-06-10 08:55:06 +00:00
|
|
|
// Always reload the config to get the latest changes
|
|
|
|
self::$config = require __DIR__ . '/../config/theme.php';
|
2025-05-23 06:58:50 +00:00
|
|
|
return self::$config;
|
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* @var string Current theme name
|
|
|
|
*/
|
|
|
|
private static $currentTheme;
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* Initialize the theme system
|
|
|
|
*/
|
|
|
|
public static function init()
|
|
|
|
{
|
2025-06-10 08:55:06 +00:00
|
|
|
// Only load config if not already loaded
|
|
|
|
if (self::$config === null) {
|
|
|
|
self::$config = require __DIR__ . '/../config/theme.php';
|
|
|
|
}
|
2025-05-15 11:54:21 +00:00
|
|
|
self::$currentTheme = self::getCurrentThemeName();
|
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* Get the current theme name
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getCurrentThemeName()
|
|
|
|
{
|
2025-06-10 08:55:06 +00:00
|
|
|
// Ensure session is started
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
|
|
Session::startSession();
|
|
|
|
}
|
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
// Check if already determined
|
|
|
|
if (self::$currentTheme !== null) {
|
|
|
|
return self::$currentTheme;
|
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
// Try to get from session first
|
2025-06-23 11:08:11 +00:00
|
|
|
$sessionTheme = isset($_SESSION['theme']) ? $_SESSION['theme'] : null;
|
2025-06-20 10:55:08 +00:00
|
|
|
if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) {
|
|
|
|
self::$currentTheme = $sessionTheme;
|
|
|
|
} else {
|
|
|
|
// Fall back to default theme
|
|
|
|
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;
|
2025-05-15 11:54:21 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:27:54 +00:00
|
|
|
// Generate URL that goes through index.php
|
2025-06-20 10:55:08 +00:00
|
|
|
global $app_root;
|
2025-06-26 11:27:54 +00:00
|
|
|
// 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);
|
2025-05-15 11:54:21 +00:00
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* Set the current theme for the session
|
|
|
|
*
|
|
|
|
* @param string $themeName
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public static function setCurrentTheme(string $themeName): bool
|
|
|
|
{
|
|
|
|
if (!self::themeExists($themeName)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2025-06-10 08:55:06 +00:00
|
|
|
// Update session
|
2025-05-15 11:54:21 +00:00
|
|
|
if (Session::isValidSession()) {
|
2025-06-26 11:48:51 +00:00
|
|
|
$_SESSION['theme'] = $themeName;
|
2025-06-10 08:55:06 +00:00
|
|
|
} else {
|
|
|
|
return false;
|
2025-05-15 11:54:21 +00:00
|
|
|
}
|
|
|
|
|
2025-06-26 11:48:51 +00:00
|
|
|
// Clear the current theme cache
|
|
|
|
self::$currentTheme = null;
|
|
|
|
|
2025-06-10 08:55:06 +00:00
|
|
|
// Update config file
|
|
|
|
$configFile = __DIR__ . '/../config/theme.php';
|
|
|
|
if (file_exists($configFile) && is_writable($configFile)) {
|
|
|
|
$config = file_get_contents($configFile);
|
|
|
|
// Update the active_theme in the config
|
|
|
|
$newConfig = preg_replace(
|
|
|
|
"/'active_theme'\s*=>\s*'[^']*'/",
|
|
|
|
"'active_theme' => '" . addslashes($themeName) . "'",
|
|
|
|
$config
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($newConfig !== $config) {
|
|
|
|
if (file_put_contents($configFile, $newConfig) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self::$currentTheme = $themeName;
|
|
|
|
return true;
|
|
|
|
}
|
2025-06-26 11:48:51 +00:00
|
|
|
|
2025-06-10 08:55:06 +00:00
|
|
|
return false;
|
2025-05-15 11:54:21 +00:00
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* Check if a theme exists
|
|
|
|
*
|
|
|
|
* @param string $themeName
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public static function themeExists(string $themeName): bool
|
|
|
|
{
|
2025-06-10 08:55:06 +00:00
|
|
|
// Default theme always exists as it uses core templates
|
|
|
|
if ($themeName === 'default') {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
$themePath = self::getThemePath($themeName);
|
|
|
|
return is_dir($themePath) && file_exists("$themePath/config.php");
|
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* 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";
|
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
2025-05-23 06:58:50 +00:00
|
|
|
* Include a theme template file
|
2025-05-15 11:54:21 +00:00
|
|
|
*
|
2025-05-23 06:58:50 +00:00
|
|
|
* @param string $template Template name without .php extension
|
2025-05-15 11:54:21 +00:00
|
|
|
* @return void
|
|
|
|
*/
|
2025-05-23 06:58:50 +00:00
|
|
|
public static function include($template)
|
2025-05-15 11:54:21 +00:00
|
|
|
{
|
2025-06-16 09:22:26 +00:00
|
|
|
global $config;
|
|
|
|
$config = $config ?? [];
|
|
|
|
|
|
|
|
$themeConfig = self::getConfig();
|
2025-05-15 11:54:21 +00:00
|
|
|
$themeName = self::getCurrentThemeName();
|
|
|
|
|
2025-05-23 06:58:50 +00:00
|
|
|
// We need this here, otherwise because this helper
|
|
|
|
// between index and the views breaks the session vars
|
|
|
|
extract($GLOBALS, EXTR_SKIP | EXTR_REFS);
|
|
|
|
|
2025-06-16 09:22:26 +00:00
|
|
|
// Ensure config is always available in templates
|
|
|
|
$config = array_merge($config, $themeConfig);
|
|
|
|
|
2025-05-23 06:58:50 +00:00
|
|
|
// For non-default themes, look in the theme directory first
|
2025-05-15 11:54:21 +00:00
|
|
|
if ($themeName !== 'default') {
|
|
|
|
$themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php';
|
|
|
|
if (file_exists($themePath)) {
|
|
|
|
include $themePath;
|
|
|
|
return;
|
|
|
|
}
|
2025-05-23 06:58:50 +00:00
|
|
|
}
|
2025-05-15 11:54:21 +00:00
|
|
|
|
2025-05-23 06:58:50 +00:00
|
|
|
// Fallback to default template location
|
|
|
|
$defaultPath = $config['paths']['templates'] . '/' . $template . '.php';
|
|
|
|
if (file_exists($defaultPath)) {
|
|
|
|
include $defaultPath;
|
|
|
|
return;
|
2025-05-15 11:54:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Log error if template not found
|
|
|
|
error_log("Template not found: {$template} in theme: {$themeName}");
|
|
|
|
}
|
|
|
|
|
2025-06-20 10:55:08 +00:00
|
|
|
|
2025-05-15 11:54:21 +00:00
|
|
|
/**
|
|
|
|
* Get all available themes
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public static function getAvailableThemes(): array
|
|
|
|
{
|
2025-06-01 07:39:39 +00:00
|
|
|
$config = self::getConfig();
|
|
|
|
$availableThemes = $config['available_themes'] ?? [];
|
2025-05-15 11:54:21 +00:00
|
|
|
$themes = [];
|
|
|
|
|
2025-06-01 07:39:39 +00:00
|
|
|
// Add default theme if not already present
|
|
|
|
if (!isset($availableThemes['default'])) {
|
|
|
|
$availableThemes['default'] = 'Default built-in theme';
|
2025-05-15 11:54:21 +00:00
|
|
|
}
|
|
|
|
|
2025-06-01 07:39:39 +00:00
|
|
|
// Verify each theme exists and has a config file
|
|
|
|
$themesDir = $config['paths']['themes'] ?? (__DIR__ . '/../../themes');
|
|
|
|
foreach ($availableThemes as $id => $name) {
|
2025-06-10 08:55:06 +00:00
|
|
|
if ($id === 'default' || (is_dir("$themesDir/$id") && file_exists("$themesDir/$id/config.php"))) {
|
2025-06-01 07:39:39 +00:00
|
|
|
$themes[$id] = $name;
|
2025-05-15 11:54:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $themes;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the theme system
|
|
|
|
Theme::init();
|