Makes theme setting per-user
parent
dfcc1dc7d8
commit
056388be71
|
@ -12,6 +12,11 @@ class User {
|
|||
private $db;
|
||||
private $rateLimiter;
|
||||
private $twoFactorAuth;
|
||||
/**
|
||||
* Cache for database schema checks
|
||||
* @var array<string,bool>
|
||||
*/
|
||||
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.
|
||||
|
|
|
@ -150,9 +150,25 @@ EOT;
|
|||
if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) {
|
||||
self::$currentTheme = $sessionTheme;
|
||||
} 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'];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// 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());
|
||||
}
|
||||
|
||||
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,
|
||||
`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`)
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue