diff --git a/app/classes/user.php b/app/classes/user.php index 849a687..d62a19e 100644 --- a/app/classes/user.php +++ b/app/classes/user.php @@ -12,6 +12,11 @@ class User { private $db; private $rateLimiter; private $twoFactorAuth; + /** + * Cache for database schema checks + * @var array + */ + private static $schemaCache = []; /** * User constructor. @@ -32,6 +37,79 @@ class User { $this->twoFactorAuth = new TwoFactorAuthentication($database); } + /** + * Check if a column exists in a given table. Results are cached per request. + * + * @param string $table + * @param string $column + * @return bool + */ + private function columnExists(string $table, string $column): bool { + $cacheKey = $table . '.' . $column; + if (isset(self::$schemaCache[$cacheKey])) { + return self::$schemaCache[$cacheKey]; + } + try { + $stmt = $this->db->prepare("SHOW COLUMNS FROM `$table` LIKE :column"); + $stmt->execute([':column' => $column]); + $exists = (bool)$stmt->fetch(PDO::FETCH_ASSOC); + self::$schemaCache[$cacheKey] = $exists; + return $exists; + } catch (Exception $e) { + // On error, assume column doesn't exist to be safe + self::$schemaCache[$cacheKey] = false; + return false; + } + } + + /** + * Get the user's preferred theme if stored in DB (user_meta.theme). Returns null if not set. + * + * @param int $userId + * @return string|null + */ + public function getUserTheme(int $userId): ?string { + if (!$this->columnExists('user_meta', 'theme')) { + return null; + } + try { + $sql = 'SELECT theme FROM user_meta WHERE user_id = :user_id LIMIT 1'; + $stmt = $this->db->prepare($sql); + $stmt->execute([':user_id' => $userId]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$row) { + return null; + } + $theme = $row['theme'] ?? null; + return ($theme !== null && $theme !== '') ? $theme : null; + } catch (Exception $e) { + return null; + } + } + + /** + * Persist the user's preferred theme in DB (user_meta.theme) when the column exists. + * Silently no-ops if the column is missing. + * + * @param int $userId + * @param string $theme + * @return bool True when stored or safely skipped; false only on explicit DB error. + */ + public function setUserTheme(int $userId, string $theme): bool { + if (!$this->columnExists('user_meta', 'theme')) { + // Column not present; treat as success to avoid breaking UX + return true; + } + try { + $sql = 'UPDATE user_meta SET theme = :theme WHERE user_id = :user_id'; + $stmt = $this->db->prepare($sql); + $ok = $stmt->execute([':theme' => $theme, ':user_id' => $userId]); + return (bool)$ok; + } catch (Exception $e) { + return false; + } + } + /** * Logs in a user by verifying credentials. diff --git a/app/helpers/theme.php b/app/helpers/theme.php index 29f9379..81a7f8e 100644 --- a/app/helpers/theme.php +++ b/app/helpers/theme.php @@ -150,8 +150,24 @@ EOT; if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) { self::$currentTheme = $sessionTheme; } else { - // Fall back to default theme - self::$currentTheme = self::$config['active_theme']; + // 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; @@ -202,7 +218,7 @@ EOT; * @param string $themeName * @return bool */ - public static function setCurrentTheme(string $themeName): bool + public static function setCurrentTheme(string $themeName, bool $persist = true): bool { if (!self::themeExists($themeName)) { return false; @@ -218,49 +234,16 @@ EOT; // Clear the current theme cache self::$currentTheme = null; - // Update config file - $configFile = __DIR__ . '/../config/theme.php'; - - // Check if config file exists and is writable - if (!file_exists($configFile)) { - error_log("Theme config file not found: $configFile"); - return false; - } - - if (!is_writable($configFile)) { - error_log("Theme config file is not writable: $configFile"); - if (isset($GLOBALS['feedback_messages'])) { - $GLOBALS['feedback_messages'][] = [ - 'type' => 'error', - 'message' => 'Cannot save theme preference: configuration file is not writable.' - ]; - } - return false; - } - - $config = file_get_contents($configFile); - if ($config === false) { - error_log("Failed to read theme config file: $configFile"); - return false; - } - - // 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) { - error_log("Failed to write to theme config file: $configFile"); - if (isset($GLOBALS['feedback_messages'])) { - $GLOBALS['feedback_messages'][] = [ - 'type' => 'error', - 'message' => 'Failed to save theme preference due to a system error.' - ]; + // 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()); } - return false; } } diff --git a/doc/database/main.sql b/doc/database/main.sql index c5c637e..5cb5b11 100644 --- a/doc/database/main.sql +++ b/doc/database/main.sql @@ -32,6 +32,7 @@ CREATE TABLE `user_meta` ( `timezone` varchar(255) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `bio` text DEFAULT NULL, + `theme` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`,`user_id`) USING BTREE, KEY `user_id` (`user_id`), CONSTRAINT `user_meta_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) diff --git a/public_html/index.php b/public_html/index.php index b988ac3..e56faa9 100644 --- a/public_html/index.php +++ b/public_html/index.php @@ -210,6 +210,18 @@ if (!$pipeline->run()) { exit; } +// Apply per-user theme from DB into session (without persisting) once user is known +if ($validSession && isset($userId) && isset($userObject) && is_object($userObject) && method_exists($userObject, 'getUserTheme')) { + try { + $dbTheme = $userObject->getUserTheme((int)$userId); + if ($dbTheme) { + \App\Helpers\Theme::setCurrentTheme($dbTheme, false); + } + } catch (\Throwable $e) { + // Non-fatal if theme load fails + } +} + // get platforms details require '../app/classes/platform.php'; $platformObject = new Platform($db);