schemaEnsured) { return; } if (!$this->hasColumn('user_profiles', 'location_tracking_preference')) { $this->pdo->exec( "ALTER TABLE user_profiles ADD COLUMN location_tracking_preference ENUM('disabled','prompt','enabled') NOT NULL DEFAULT 'prompt' AFTER lng" ); $this->columnCache['user_profiles.location_tracking_preference'] = true; } $avatarColumns = [ 'avatar_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_style VARCHAR(40) NOT NULL DEFAULT 'lorelei' AFTER location_tracking_preference", 'avatar_seed' => "ALTER TABLE user_profiles ADD COLUMN avatar_seed VARCHAR(100) NOT NULL DEFAULT '' AFTER avatar_style", 'avatar_config_json' => "ALTER TABLE user_profiles ADD COLUMN avatar_config_json TEXT NULL AFTER avatar_seed", 'avatar_preset' => "ALTER TABLE user_profiles ADD COLUMN avatar_preset VARCHAR(64) NOT NULL DEFAULT 'papa-kind-treff' AFTER location_tracking_preference", 'avatar_lorelei_eyes_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_eyes_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_preset", 'avatar_lorelei_eyebrows_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_eyebrows_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_eyes_variant", 'avatar_lorelei_mouth_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_mouth_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_eyebrows_variant", 'avatar_lorelei_glasses_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_glasses_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_mouth_variant", 'avatar_lorelei_hair_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_hair_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_glasses_variant", 'avatar_lorelei_beard_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_beard_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_hair_variant", 'avatar_lorelei_earrings_variant' => "ALTER TABLE user_profiles ADD COLUMN avatar_lorelei_earrings_variant VARCHAR(24) NOT NULL DEFAULT '' AFTER avatar_lorelei_beard_variant", ]; foreach ($avatarColumns as $column => $sql) { if (!$this->hasColumn('user_profiles', $column)) { $this->pdo->exec($sql); $this->columnCache['user_profiles.' . $column] = true; } } $this->schemaEnsured = true; } public function getLocationTrackingPreference(int $userId): string { if ($userId <= 0) { return 'prompt'; } $this->ensureSchema(); $stmt = $this->pdo->prepare('SELECT location_tracking_preference FROM user_profiles WHERE user_id = :id LIMIT 1'); $stmt->execute(['id' => $userId]); $value = (string)($stmt->fetchColumn() ?: 'prompt'); return in_array($value, ['disabled', 'prompt', 'enabled'], true) ? $value : 'prompt'; } public function updateLocationTrackingPreference(int $userId, string $preference): void { if ($userId <= 0) { return; } $this->ensureSchema(); $preference = in_array($preference, ['disabled', 'prompt', 'enabled'], true) ? $preference : 'prompt'; $stmt = $this->pdo->prepare('UPDATE user_profiles SET location_tracking_preference = :pref, updated_at = NOW() WHERE user_id = :id'); $stmt->execute([ 'pref' => $preference, 'id' => $userId, ]); } public function updateAvatar(int $userId, array $avatarConfig): void { if ($userId <= 0) { return; } $this->ensureSchema(); $avatar = AvatarManager::normalizeSubmission($avatarConfig, $userId); $legacy = $avatar['avatar_style'] === Lorelei::key() ? Lorelei::legacyColumnsFromConfig($avatar['avatar_seed'], $avatar['avatar_config_array']) : Lorelei::legacyColumnsFromConfig($avatar['avatar_seed'], Lorelei::defaultConfig()); $stmt = $this->pdo->prepare( 'UPDATE user_profiles SET avatar_style = :avatarStyle, avatar_seed = :avatarSeed, avatar_config_json = :avatarConfigJson, avatar_preset = :preset, avatar_lorelei_eyes_variant = :eyesVariant, avatar_lorelei_eyebrows_variant = :eyebrowsVariant, avatar_lorelei_mouth_variant = :mouthVariant, avatar_lorelei_glasses_variant = :glassesVariant, avatar_lorelei_hair_variant = :hairVariant, avatar_lorelei_beard_variant = :beardVariant, avatar_lorelei_earrings_variant = :earringsVariant, updated_at = NOW() WHERE user_id = :id' ); $stmt->execute([ 'avatarStyle' => $avatar['avatar_style'], 'avatarSeed' => $avatar['avatar_seed'], 'avatarConfigJson' => $avatar['avatar_config_json'], 'preset' => $legacy['avatar_preset'], 'eyesVariant' => $legacy['avatar_lorelei_eyes_variant'], 'eyebrowsVariant' => $legacy['avatar_lorelei_eyebrows_variant'], 'mouthVariant' => $legacy['avatar_lorelei_mouth_variant'], 'glassesVariant' => $legacy['avatar_lorelei_glasses_variant'], 'hairVariant' => $legacy['avatar_lorelei_hair_variant'], 'beardVariant' => $legacy['avatar_lorelei_beard_variant'], 'earringsVariant' => $legacy['avatar_lorelei_earrings_variant'], 'id' => $userId, ]); } private function hasColumn(string $table, string $column): bool { $cacheKey = $table . '.' . $column; if (array_key_exists($cacheKey, $this->columnCache)) { return $this->columnCache[$cacheKey]; } $stmt = $this->pdo->prepare(" SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :tableName AND COLUMN_NAME = :columnName "); $stmt->execute([ 'tableName' => $table, 'columnName' => $column, ]); return $this->columnCache[$cacheKey] = ((int)$stmt->fetchColumn() > 0); } }