Makes theme setting per-user
parent
dfcc1dc7d8
commit
056388be71
|
@ -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.
|
||||||
|
|
|
@ -150,8 +150,24 @@ 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
|
||||||
self::$currentTheme = self::$config['active_theme'];
|
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;
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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`)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue