package api import ( "database/sql" "encoding/json" "fmt" "log" "net/http" "strings" "time" "unicode/utf8" "pinepods/gpodder-api/internal/db" "pinepods/gpodder-api/internal/models" "github.com/gin-gonic/gin" ) // Constants for settings const ( MAX_SETTING_KEY_LENGTH = 255 MAX_SETTING_VALUE_LENGTH = 8192 MAX_SETTINGS_PER_REQUEST = 50 ) // Known settings that trigger behavior var knownSettings = map[string]map[string]bool{ "account": { "public_profile": true, "store_user_agent": true, "public_subscriptions": true, "color_theme": true, "default_subscribe_all": true, }, "episode": { "is_favorite": true, "played": true, "current_position": true, }, "podcast": { "public_subscription": true, "auto_download": true, "episode_sort": true, }, "device": { "auto_update": true, "update_interval": true, "wifi_only_downloads": true, "max_episodes_per_feed": true, }, } // Validation interfaces and functions // ValueValidator defines interface for validating settings values type ValueValidator interface { Validate(value interface{}) bool } // BooleanValidator validates boolean values type BooleanValidator struct{} func (v BooleanValidator) Validate(value interface{}) bool { _, ok := value.(bool) return ok } // IntValidator validates integer values type IntValidator struct { Min int Max int } func (v IntValidator) Validate(value interface{}) bool { num, ok := value.(float64) // JSON numbers are parsed as float64 if !ok { return false } // Check if it's a whole number if num != float64(int(num)) { return false } // Check range if specified intVal := int(num) if v.Min != 0 || v.Max != 0 { if intVal < v.Min || (v.Max != 0 && intVal > v.Max) { return false } } return true } // StringValidator validates string values type StringValidator struct { AllowedValues []string MaxLength int } func (v StringValidator) Validate(value interface{}) bool { str, ok := value.(string) if !ok { return false } // Check maximum length if specified if v.MaxLength > 0 && utf8.RuneCountInString(str) > v.MaxLength { return false } // Check allowed values if specified if len(v.AllowedValues) > 0 { for _, allowed := range v.AllowedValues { if str == allowed { return true } } return false } return true } // validation rules for specific settings var settingValidators = map[string]map[string]ValueValidator{ "account": { "public_profile": BooleanValidator{}, "store_user_agent": BooleanValidator{}, "public_subscriptions": BooleanValidator{}, "default_subscribe_all": BooleanValidator{}, "color_theme": StringValidator{AllowedValues: []string{"light", "dark", "system"}, MaxLength: 10}, }, "episode": { "is_favorite": BooleanValidator{}, "played": BooleanValidator{}, "current_position": IntValidator{Min: 0}, }, "podcast": { "public_subscription": BooleanValidator{}, "auto_download": BooleanValidator{}, "episode_sort": StringValidator{AllowedValues: []string{"newest_first", "oldest_first", "title"}, MaxLength: 20}, }, "device": { "auto_update": BooleanValidator{}, "update_interval": IntValidator{Min: 10, Max: 1440}, // 10 minutes to 24 hours "wifi_only_downloads": BooleanValidator{}, "max_episodes_per_feed": IntValidator{Min: 1, Max: 1000}, }, } // validateSettingValue validates a setting value based on its scope and key func validateSettingValue(scope, key string, value interface{}) (bool, string) { // Maximum setting value length check jsonValue, err := json.Marshal(value) if err != nil { return false, "Failed to serialize setting value" } if len(jsonValue) > MAX_SETTING_VALUE_LENGTH { return false, "Setting value exceeds maximum length" } // Check if we have a specific validator for this setting if validators, ok := settingValidators[scope]; ok { if validator, ok := validators[key]; ok { if !validator.Validate(value) { return false, "Setting value failed validation for the specified scope and key" } } } return true, "" } // getSettings handles GET /api/2/settings/{username}/{scope}.json func getSettings(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Get scope from URL scope := c.Param("scope") if scope == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Scope is required"}) return } // Validate scope if scope != "account" && scope != "device" && scope != "podcast" && scope != "episode" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid scope. Valid values are: account, device, podcast, episode", }) return } // Get optional query parameters deviceID := c.Query("device") podcastURL := c.Query("podcast") episodeURL := c.Query("episode") // Validate parameters based on scope if scope == "device" && deviceID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Device ID is required for device scope"}) return } if scope == "podcast" && podcastURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL is required for podcast scope"}) return } if scope == "episode" && (podcastURL == "" || episodeURL == "") { c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL and Episode URL are required for episode scope"}) return } // Build query based on scope var query string var args []interface{} switch scope { case "account": if database.IsPostgreSQLDB() { query = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope) } else { query = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope) } case "device": // Get device ID from name var deviceIDInt int var deviceQuery string if database.IsPostgreSQLDB() { deviceQuery = ` SELECT DeviceID FROM "GpodderDevices" WHERE UserID = $1 AND DeviceName = $2 AND IsActive = true ` } else { deviceQuery = ` SELECT DeviceID FROM GpodderDevices WHERE UserID = ? AND DeviceName = ? AND IsActive = true ` } err := database.QueryRow(deviceQuery, userID, deviceID).Scan(&deviceIDInt) if err != nil { if err == sql.ErrNoRows { log.Printf("Device not found: %s", deviceID) c.JSON(http.StatusNotFound, gin.H{"error": "Device not found or not active"}) } else { log.Printf("Error getting device ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"}) } return } if database.IsPostgreSQLDB() { query = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND DeviceID = $3 AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, deviceIDInt) } else { query = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND DeviceID = ? AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, deviceIDInt) } case "podcast": // Validate podcast URL if !isValidURL(podcastURL) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast URL"}) return } if database.IsPostgreSQLDB() { query = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND DeviceID IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, podcastURL) } else { query = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND DeviceID IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, podcastURL) } case "episode": // Validate URLs if !isValidURL(podcastURL) || !isValidURL(episodeURL) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast or episode URL"}) return } if database.IsPostgreSQLDB() { query = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND EpisodeURL = $4 AND DeviceID IS NULL ` args = append(args, userID, scope, podcastURL, episodeURL) } else { query = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND EpisodeURL = ? AND DeviceID IS NULL ` args = append(args, userID, scope, podcastURL, episodeURL) } } // Query settings rows, err := database.Query(query, args...) if err != nil { log.Printf("Error querying settings: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"}) return } defer rows.Close() // Build settings map settings := make(map[string]interface{}) for rows.Next() { var key, value string if err := rows.Scan(&key, &value); err != nil { log.Printf("Error scanning setting row: %v", err) continue } // Try to unmarshal as JSON, fallback to string if not valid JSON var jsonValue interface{} if err := json.Unmarshal([]byte(value), &jsonValue); err != nil { // Not valid JSON, use as string settings[key] = value } else { // Valid JSON, use parsed value settings[key] = jsonValue } } if err := rows.Err(); err != nil { log.Printf("Error iterating setting rows: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"}) return } // Return settings c.JSON(http.StatusOK, settings) } } // isValidURL performs basic URL validation func isValidURL(urlStr string) bool { // Check if empty if urlStr == "" { return false } // Must start with http:// or https:// if !strings.HasPrefix(strings.ToLower(urlStr), "http://") && !strings.HasPrefix(strings.ToLower(urlStr), "https://") { return false } // Basic length check if len(urlStr) < 10 || len(urlStr) > 2048 { return false } return true } // saveSettings handles POST /api/2/settings/{username}/{scope}.json func saveSettings(database *db.Database) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Get scope from URL scope := c.Param("scope") if scope == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Scope is required"}) return } // Validate scope if scope != "account" && scope != "device" && scope != "podcast" && scope != "episode" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid scope. Valid values are: account, device, podcast, episode", }) return } // Get optional query parameters deviceName := c.Query("device") podcastURL := c.Query("podcast") episodeURL := c.Query("episode") // Validate parameters based on scope if scope == "device" && deviceName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Device ID is required for device scope"}) return } if scope == "podcast" && podcastURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL is required for podcast scope"}) return } if scope == "episode" && (podcastURL == "" || episodeURL == "") { c.JSON(http.StatusBadRequest, gin.H{"error": "Podcast URL and Episode URL are required for episode scope"}) return } // Validate URLs if scope == "podcast" && !isValidURL(podcastURL) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast URL"}) return } if scope == "episode" && (!isValidURL(podcastURL) || !isValidURL(episodeURL)) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid podcast or episode URL"}) return } // Parse request body var req models.SettingsRequest if err := c.ShouldBindJSON(&req); err != nil { log.Printf("Error parsing request: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: expected a JSON object with 'set' and 'remove' properties"}) return } // Validate request size if len(req.Set) > MAX_SETTINGS_PER_REQUEST || len(req.Remove) > MAX_SETTINGS_PER_REQUEST { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Too many settings in request. Maximum allowed: %d", MAX_SETTINGS_PER_REQUEST), }) return } // Process device ID if needed var deviceID *int if scope == "device" { var deviceIDInt int var deviceQuery string if database.IsPostgreSQLDB() { deviceQuery = ` SELECT DeviceID FROM "GpodderDevices" WHERE UserID = $1 AND DeviceName = $2 AND IsActive = true ` } else { deviceQuery = ` SELECT DeviceID FROM GpodderDevices WHERE UserID = ? AND DeviceName = ? AND IsActive = true ` } err := database.QueryRow(deviceQuery, userID, deviceName).Scan(&deviceIDInt) if err != nil { if err == sql.ErrNoRows { // Create the device if it doesn't exist if database.IsPostgreSQLDB() { deviceQuery = ` INSERT INTO "GpodderDevices" (UserID, DeviceName, DeviceType, IsActive, LastSync) VALUES ($1, $2, 'other', true, $3) RETURNING DeviceID ` err = database.QueryRow(deviceQuery, userID, deviceName, time.Now()).Scan(&deviceIDInt) } else { deviceQuery = ` INSERT INTO GpodderDevices (UserID, DeviceName, DeviceType, IsActive, LastSync) VALUES (?, ?, 'other', true, ?) ` result, err := database.Exec(deviceQuery, userID, deviceName, "other", time.Now()) if err != nil { log.Printf("Error creating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"}) return } lastID, err := result.LastInsertId() if err != nil { log.Printf("Error getting last insert ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"}) return } deviceIDInt = int(lastID) } if err != nil { log.Printf("Error creating device: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create device"}) return } } else { log.Printf("Error getting device ID: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get device"}) return } } deviceID = &deviceIDInt } // Begin transaction tx, err := database.Begin() if err != nil { log.Printf("Error beginning transaction: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"}) return } defer func() { if err != nil { tx.Rollback() return } }() // Process settings to set for key, value := range req.Set { // Validate key if len(key) == 0 || len(key) > MAX_SETTING_KEY_LENGTH { log.Printf("Invalid setting key length: %s", key) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Invalid setting key: must be between 1 and %d characters", MAX_SETTING_KEY_LENGTH), }) return } // Allow only letters, numbers, underscores and hyphens if !isValidSettingKey(key) { log.Printf("Invalid setting key: %s", key) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid setting key: must contain only letters, numbers, underscores and hyphens", }) return } // Validate value valid, errMsg := validateSettingValue(scope, key, value) if !valid { log.Printf("Invalid setting value for key %s: %s", key, errMsg) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Invalid value for key '%s': %s", key, errMsg), }) return } // Convert value to JSON string jsonValue, err := json.Marshal(value) if err != nil { log.Printf("Error marshaling value to JSON: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid value for key: " + key}) return } // Build query based on scope var query string var args []interface{} switch scope { case "account": if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderSyncSettings" (UserID, Scope, SettingKey, SettingValue, LastUpdated) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (UserID, Scope, SettingKey) WHERE DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL DO UPDATE SET SettingValue = $4, LastUpdated = $5 ` args = append(args, userID, scope, key, string(jsonValue), time.Now()) } else { query = ` INSERT INTO GpodderSyncSettings (UserID, Scope, SettingKey, SettingValue, LastUpdated) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated) ` args = append(args, userID, scope, key, string(jsonValue), time.Now()) } case "device": if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderSyncSettings" (UserID, Scope, DeviceID, SettingKey, SettingValue, LastUpdated) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (UserID, Scope, SettingKey, DeviceID) WHERE PodcastURL IS NULL AND EpisodeURL IS NULL DO UPDATE SET SettingValue = $5, LastUpdated = $6 ` args = append(args, userID, scope, deviceID, key, string(jsonValue), time.Now()) } else { query = ` INSERT INTO GpodderSyncSettings (UserID, Scope, DeviceID, SettingKey, SettingValue, LastUpdated) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated) ` args = append(args, userID, scope, deviceID, key, string(jsonValue), time.Now()) } case "podcast": if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderSyncSettings" (UserID, Scope, PodcastURL, SettingKey, SettingValue, LastUpdated) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (UserID, Scope, SettingKey, PodcastURL) WHERE DeviceID IS NULL AND EpisodeURL IS NULL DO UPDATE SET SettingValue = $5, LastUpdated = $6 ` args = append(args, userID, scope, podcastURL, key, string(jsonValue), time.Now()) } else { query = ` INSERT INTO GpodderSyncSettings (UserID, Scope, PodcastURL, SettingKey, SettingValue, LastUpdated) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated) ` args = append(args, userID, scope, podcastURL, key, string(jsonValue), time.Now()) } case "episode": if database.IsPostgreSQLDB() { query = ` INSERT INTO "GpodderSyncSettings" (UserID, Scope, PodcastURL, EpisodeURL, SettingKey, SettingValue, LastUpdated) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (UserID, Scope, SettingKey, PodcastURL, EpisodeURL) WHERE DeviceID IS NULL DO UPDATE SET SettingValue = $6, LastUpdated = $7 ` args = append(args, userID, scope, podcastURL, episodeURL, key, string(jsonValue), time.Now()) } else { query = ` INSERT INTO GpodderSyncSettings (UserID, Scope, PodcastURL, EpisodeURL, SettingKey, SettingValue, LastUpdated) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE SettingValue = VALUES(SettingValue), LastUpdated = VALUES(LastUpdated) ` args = append(args, userID, scope, podcastURL, episodeURL, key, string(jsonValue), time.Now()) } } // Execute query _, err = tx.Exec(query, args...) if err != nil { log.Printf("Error setting value for key %s: %v", key, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"}) return } } // Process settings to remove for _, key := range req.Remove { // Validate key if len(key) == 0 || len(key) > MAX_SETTING_KEY_LENGTH { log.Printf("Invalid setting key length: %s", key) c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Invalid setting key: must be between 1 and %d characters", MAX_SETTING_KEY_LENGTH), }) return } // Allow only letters, numbers, underscores and hyphens if !isValidSettingKey(key) { log.Printf("Invalid setting key: %s", key) c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid setting key: must contain only letters, numbers, underscores and hyphens", }) return } // Build query based on scope var query string var args []interface{} switch scope { case "account": if database.IsPostgreSQLDB() { query = ` DELETE FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND SettingKey = $3 AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, key) } else { query = ` DELETE FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND SettingKey = ? AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, key) } case "device": if database.IsPostgreSQLDB() { query = ` DELETE FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND DeviceID = $3 AND SettingKey = $4 AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, deviceID, key) } else { query = ` DELETE FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND DeviceID = ? AND SettingKey = ? AND PodcastURL IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, deviceID, key) } case "podcast": if database.IsPostgreSQLDB() { query = ` DELETE FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND SettingKey = $4 AND DeviceID IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, podcastURL, key) } else { query = ` DELETE FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND SettingKey = ? AND DeviceID IS NULL AND EpisodeURL IS NULL ` args = append(args, userID, scope, podcastURL, key) } case "episode": if database.IsPostgreSQLDB() { query = ` DELETE FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND EpisodeURL = $4 AND SettingKey = $5 AND DeviceID IS NULL ` args = append(args, userID, scope, podcastURL, episodeURL, key) } else { query = ` DELETE FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND EpisodeURL = ? AND SettingKey = ? AND DeviceID IS NULL ` args = append(args, userID, scope, podcastURL, episodeURL, key) } } // Execute query _, err = tx.Exec(query, args...) if err != nil { log.Printf("Error removing key %s: %v", key, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"}) return } } // Commit transaction if err = tx.Commit(); err != nil { log.Printf("Error committing transaction: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"}) return } // Query all settings for the updated response var queryAll string var argsAll []interface{} switch scope { case "account": if database.IsPostgreSQLDB() { queryAll = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL ` argsAll = append(argsAll, userID, scope) } else { queryAll = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND DeviceID IS NULL AND PodcastURL IS NULL AND EpisodeURL IS NULL ` argsAll = append(argsAll, userID, scope) } case "device": if database.IsPostgreSQLDB() { queryAll = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND DeviceID = $3 AND PodcastURL IS NULL AND EpisodeURL IS NULL ` argsAll = append(argsAll, userID, scope, deviceID) } else { queryAll = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND DeviceID = ? AND PodcastURL IS NULL AND EpisodeURL IS NULL ` argsAll = append(argsAll, userID, scope, deviceID) } case "podcast": if database.IsPostgreSQLDB() { queryAll = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND DeviceID IS NULL AND EpisodeURL IS NULL ` argsAll = append(argsAll, userID, scope, podcastURL) } else { queryAll = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND DeviceID IS NULL AND EpisodeURL IS NULL ` argsAll = append(argsAll, userID, scope, podcastURL) } case "episode": if database.IsPostgreSQLDB() { queryAll = ` SELECT SettingKey, SettingValue FROM "GpodderSyncSettings" WHERE UserID = $1 AND Scope = $2 AND PodcastURL = $3 AND EpisodeURL = $4 AND DeviceID IS NULL ` argsAll = append(argsAll, userID, scope, podcastURL, episodeURL) } else { queryAll = ` SELECT SettingKey, SettingValue FROM GpodderSyncSettings WHERE UserID = ? AND Scope = ? AND PodcastURL = ? AND EpisodeURL = ? AND DeviceID IS NULL ` argsAll = append(argsAll, userID, scope, podcastURL, episodeURL) } } // Query all settings rows, err := database.Query(queryAll, argsAll...) if err != nil { log.Printf("Error querying all settings: %v", err) c.JSON(http.StatusOK, gin.H{}) // Return empty object in case of error return } defer rows.Close() // Build settings map settings := make(map[string]interface{}) for rows.Next() { var key, value string if err := rows.Scan(&key, &value); err != nil { log.Printf("Error scanning setting row: %v", err) continue } // Try to unmarshal as JSON, fallback to string if not valid JSON var jsonValue interface{} if err := json.Unmarshal([]byte(value), &jsonValue); err != nil { // Not valid JSON, use as string settings[key] = value } else { // Valid JSON, use parsed value settings[key] = jsonValue } } if err := rows.Err(); err != nil { log.Printf("Error iterating setting rows: %v", err) c.JSON(http.StatusOK, gin.H{}) // Return empty object in case of error return } // Return updated settings c.JSON(http.StatusOK, settings) } } // isValidSettingKey checks if the key contains only valid characters func isValidSettingKey(key string) bool { for _, r := range key { if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9') && r != '_' && r != '-' { return false } } return true } // toggleGpodderAPI is a Pinepods-specific extension to enable/disable the gpodder API for a user func toggleGpodderAPI(database *db.PostgresDB) gin.HandlerFunc { return func(c *gin.Context) { // Get user ID from context (set by AuthMiddleware) userID, exists := c.Get("userID") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // Parse request body var req struct { Enable bool `json:"enable"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Set Pod_Sync_Type based on enable flag var podSyncType string if req.Enable { // Check if external gpodder sync is already enabled var currentSyncType string err := database.QueryRow(` SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1 `, userID).Scan(¤tSyncType) if err != nil { log.Printf("Error getting current sync type: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle gpodder API"}) return } if currentSyncType == "external" { podSyncType = "both" } else { podSyncType = "gpodder" } } else { // Check if external gpodder sync is enabled var currentSyncType string err := database.QueryRow(` SELECT Pod_Sync_Type FROM "Users" WHERE UserID = $1 `, userID).Scan(¤tSyncType) if err != nil { log.Printf("Error getting current sync type: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle gpodder API"}) return } if currentSyncType == "both" { podSyncType = "external" } else { podSyncType = "None" } } // Update user's Pod_Sync_Type _, err := database.Exec(` UPDATE "Users" SET Pod_Sync_Type = $1 WHERE UserID = $2 `, podSyncType, userID) if err != nil { log.Printf("Error updating Pod_Sync_Type: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to toggle gpodder API"}) return } // Return success response c.JSON(http.StatusOK, gin.H{ "enabled": req.Enable, "sync_type": podSyncType, }) } }