Makes theme setting per-user

main
Yasen Pramatarov 2025-09-24 16:56:14 +03:00
parent dfcc1dc7d8
commit 056388be71
4 changed files with 119 additions and 45 deletions

View File

@ -12,6 +12,11 @@ class User {
private $db; private $db;
private $rateLimiter; private $rateLimiter;
private $twoFactorAuth; private $twoFactorAuth;
/**
* Cache for database schema checks
* @var array<string,bool>
*/
private static $schemaCache = [];
/** /**
* User constructor. * User constructor.
@ -32,6 +37,79 @@ class User {
$this->twoFactorAuth = new TwoFactorAuthentication($database); $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. * Logs in a user by verifying credentials.

View File

@ -150,9 +150,25 @@ EOT;
if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) { if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) {
self::$currentTheme = $sessionTheme; self::$currentTheme = $sessionTheme;
} else { } else {
// Fall back to default 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']; self::$currentTheme = self::$config['active_theme'];
} }
}
return self::$currentTheme; return self::$currentTheme;
} }
@ -202,7 +218,7 @@ EOT;
* @param string $themeName * @param string $themeName
* @return bool * @return bool
*/ */
public static function setCurrentTheme(string $themeName): bool public static function setCurrentTheme(string $themeName, bool $persist = true): bool
{ {
if (!self::themeExists($themeName)) { if (!self::themeExists($themeName)) {
return false; return false;
@ -218,49 +234,16 @@ EOT;
// Clear the current theme cache // Clear the current theme cache
self::$currentTheme = null; self::$currentTheme = null;
// Update config file // Persist per-user preference in DB when available and requested
$configFile = __DIR__ . '/../config/theme.php'; if ($persist && Session::isValidSession() && isset($_SESSION['user_id'])) {
// Try to use existing user object if available
// Check if config file exists and is writable if (isset($GLOBALS['userObject']) && is_object($GLOBALS['userObject']) && method_exists($GLOBALS['userObject'], 'setUserTheme')) {
if (!file_exists($configFile)) { try {
error_log("Theme config file not found: $configFile"); $GLOBALS['userObject']->setUserTheme((int)$_SESSION['user_id'], $themeName);
return false; } catch (\Throwable $e) {
// Non-fatal: keep session theme even if DB save fails
error_log('Failed to persist user theme: ' . $e->getMessage());
} }
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.'
];
}
return false;
} }
} }

View File

@ -32,6 +32,7 @@ CREATE TABLE `user_meta` (
`timezone` varchar(255) DEFAULT NULL, `timezone` varchar(255) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL,
`bio` text DEFAULT NULL, `bio` text DEFAULT NULL,
`theme` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`,`user_id`) USING BTREE, PRIMARY KEY (`id`,`user_id`) USING BTREE,
KEY `user_id` (`user_id`), KEY `user_id` (`user_id`),
CONSTRAINT `user_meta_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) CONSTRAINT `user_meta_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)

View File

@ -210,6 +210,18 @@ if (!$pipeline->run()) {
exit; 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 // get platforms details
require '../app/classes/platform.php'; require '../app/classes/platform.php';
$platformObject = new Platform($db); $platformObject = new Platform($db);