package handlers import ( "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "gorm.io/gorm" "go_blog/models" ) // mbToBytes converts a megabyte count (string) to bytes. Returns 0 on parse // failure. Values <= 0 are treated as 0 (meaning "use default" for per-type // limits). func mbToBytes(s string) int64 { s = strings.TrimSpace(s) if s == "" { return 0 } n, err := strconv.ParseFloat(s, 64) if err != nil { return 0 } return int64(n * 1024 * 1024) } // bytesToMB renders a byte count as megabytes (one decimal) for form display. func bytesToMB(b int64) string { return fmt.Sprintf("%.1f", float64(b)/float64(1024*1024)) } // userIDFromSession extracts the logged-in user's ID, or 0 if absent. It // defends against int/uint/int64/float64 storage in the session. func userIDFromSession(c *gin.Context) uint { session := sessions.Default(c) userID := session.Get("user_id") if userID == nil { return 0 } switch v := userID.(type) { case uint: return v case int: return uint(v) case int64: return uint(v) case float64: return uint(v) } return 0 } // ---------------- Site settings ---------------- // SiteSettingsPage renders the site display settings form. func SiteSettingsPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) var s models.SiteSetting if err := db.First(&s, 1).Error; err != nil { s = models.SiteSetting{ID: 1} } data := DefaultData(c) data["Title"] = tr["settings_site_title"] data["Site"] = s // Pre-compute derived values so the template never invokes methods on // an interface{}-wrapped struct (which Go templates cannot resolve). data["SiteLogoIsURL"] = s.LogoIsURL() data["SiteFaviconIsURL"] = s.FaviconIsURL() if msg := c.Query("saved"); msg == "1" { data["Success"] = tr["settings_saved"] } c.HTML(http.StatusOK, "settings_site", data) } } // SiteSettingsSave handles logo upload and text fields for site settings. func SiteSettingsSave(db *gorm.DB, storagePath string) gin.HandlerFunc { return func(c *gin.Context) { var s models.SiteSetting if err := db.First(&s, 1).Error; err != nil { s = models.SiteSetting{ID: 1} } s.LogoTextZh = strings.TrimSpace(c.PostForm("logo_text_zh")) s.LogoTextEn = strings.TrimSpace(c.PostForm("logo_text_en")) s.HeaderTextZh = strings.TrimSpace(c.PostForm("header_text_zh")) s.HeaderTextEn = strings.TrimSpace(c.PostForm("header_text_en")) s.HomeWelcomeZh = strings.TrimSpace(c.PostForm("home_welcome_zh")) s.HomeWelcomeEn = strings.TrimSpace(c.PostForm("home_welcome_en")) s.HomeSubtitleZh = strings.TrimSpace(c.PostForm("home_subtitle_zh")) s.HomeSubtitleEn = strings.TrimSpace(c.PostForm("home_subtitle_en")) s.FooterTextZh = strings.TrimSpace(c.PostForm("footer_text_zh")) s.FooterTextEn = strings.TrimSpace(c.PostForm("footer_text_en")) s.AllowRegistration = c.PostForm("allow_registration") == "1" s.UpdatedBy = userIDFromSession(c) // Favicon upload (optional). A favicon_url form field takes precedence over an // uploaded file, so admins can set either a local file or an external link. if faviconURL := strings.TrimSpace(c.PostForm("favicon_url")); faviconURL != "" { s.Favicon = faviconURL } else if file, header, err := c.Request.FormFile("favicon"); err == nil { defer file.Close() check := ValidateUpload(header) if !check.OK || check.Type.Category != models.CategoryImage { c.Redirect(http.StatusFound, "/admin/settings/site") return } logoDir := filepath.Join(storagePath, "logos") os.MkdirAll(logoDir, 0755) // Remove the previous local favicon (skip external URLs). if s.Favicon != "" && !s.FaviconIsURL() { os.Remove(filepath.Join(logoDir, s.Favicon)) } ext := strings.ToLower(filepath.Ext(header.Filename)) savedName := fmt.Sprintf("favicon%s", ext) dst, err := os.Create(filepath.Join(logoDir, savedName)) if err != nil { c.Redirect(http.StatusFound, "/admin/settings/site") return } defer dst.Close() if _, err := io.Copy(dst, file); err != nil { c.Redirect(http.StatusFound, "/admin/settings/site") return } s.Favicon = savedName } // Remove favicon entirely if requested. if c.PostForm("favicon_clear") == "1" { if s.Favicon != "" && !s.FaviconIsURL() { os.Remove(filepath.Join(storagePath, "logos", s.Favicon)) } s.Favicon = "" } // Logo upload (optional). A logo_url form field takes precedence over an // uploaded file, so admins can set either a local file or an external link. if logoURL := strings.TrimSpace(c.PostForm("logo_url")); logoURL != "" { s.Logo = logoURL } else if file, header, err := c.Request.FormFile("logo"); err == nil { defer file.Close() check := ValidateUpload(header) if !check.OK || check.Type.Category != models.CategoryImage { c.Redirect(http.StatusFound, "/admin/settings/site") return } logoDir := filepath.Join(storagePath, "logos") os.MkdirAll(logoDir, 0755) // Remove the previous local logo (skip external URLs). if s.Logo != "" && !s.LogoIsURL() { os.Remove(filepath.Join(logoDir, s.Logo)) } ext := strings.ToLower(filepath.Ext(header.Filename)) savedName := fmt.Sprintf("logo%s", ext) dst, err := os.Create(filepath.Join(logoDir, savedName)) if err != nil { c.Redirect(http.StatusFound, "/admin/settings/site") return } defer dst.Close() if _, err := io.Copy(dst, file); err != nil { c.Redirect(http.StatusFound, "/admin/settings/site") return } s.Logo = savedName } // Remove logo entirely if requested. if c.PostForm("logo_clear") == "1" { if s.Logo != "" && !s.LogoIsURL() { os.Remove(filepath.Join(storagePath, "logos", s.Logo)) } s.Logo = "" } if err := db.Save(&s).Error; err != nil { c.Redirect(http.StatusFound, "/admin/settings/site") return } models.RefreshConfigCache(db) c.Redirect(http.StatusFound, "/admin/settings/site?saved=1") } } // ---------------- Upload settings ---------------- // fileTypeView augments an UploadFileType with a pre-rendered max-size MB // string for the template (avoids needing a template FuncMap for division). type fileTypeView struct { models.UploadFileType MaxSizeMB string } // UploadSettingsPage renders the upload policy + file-type management page. func UploadSettingsPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) var u models.UploadConfig if err := db.First(&u, 1).Error; err != nil { u = models.UploadConfig{ID: 1, Enabled: true, DefaultMaxSize: models.DefaultUploadMaxSize, StorageDir: "attachments"} } var types []models.UploadFileType db.Order("category asc, sort asc, id asc").Find(&types) views := make([]fileTypeView, 0, len(types)) for _, t := range types { views = append(views, fileTypeView{UploadFileType: t, MaxSizeMB: bytesToMB(t.MaxSize)}) } data := DefaultData(c) data["Title"] = tr["settings_upload_title"] data["Upload"] = u data["FileTypes"] = views data["DefaultMaxSizeMB"] = bytesToMB(u.DefaultMaxSize) if msg := c.Query("saved"); msg == "1" { data["Success"] = tr["settings_saved"] } c.HTML(http.StatusOK, "settings_upload", data) } } // UploadSettingsSave dispatches upload-config and file-type actions. func UploadSettingsSave(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { switch c.PostForm("action") { case "save_config": saveUploadConfig(db, c) case "add_type": addUploadFileType(db, c) case "toggle_type": toggleUploadFileType(db, c) case "size_type": sizeUploadFileType(db, c) case "delete_type": deleteUploadFileType(db, c) } models.RefreshConfigCache(db) c.Redirect(http.StatusFound, "/admin/settings/upload?saved=1") } } func saveUploadConfig(db *gorm.DB, c *gin.Context) { var u models.UploadConfig if err := db.First(&u, 1).Error; err != nil { u = models.UploadConfig{ID: 1} } u.Enabled = c.PostForm("enabled") == "1" u.DefaultMaxSize = mbToBytes(c.PostForm("default_max_size")) if u.DefaultMaxSize <= 0 { u.DefaultMaxSize = models.DefaultUploadMaxSize } if dir := strings.TrimSpace(c.PostForm("storage_dir")); dir != "" { u.StorageDir = dir } u.UpdatedBy = userIDFromSession(c) db.Save(&u) } func addUploadFileType(db *gorm.DB, c *gin.Context) { ext := strings.ToLower(strings.TrimSpace(c.PostForm("extension"))) if ext == "" { return } if !strings.HasPrefix(ext, ".") { ext = "." + ext } t := models.UploadFileType{ Extension: ext, MimeType: strings.TrimSpace(c.PostForm("mime_type")), Category: strings.TrimSpace(c.PostForm("category")), MaxSize: mbToBytes(c.PostForm("max_size")), Enabled: c.PostForm("enabled") != "0", Sort: 50, } if t.Category == "" { t.Category = models.CategoryOther } // Ignore duplicate-extension errors silently. db.Where("extension = ?", t.Extension).FirstOrCreate(&t) } func toggleUploadFileType(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) var t models.UploadFileType if db.First(&t, id).Error != nil { return } t.Enabled = !t.Enabled db.Save(&t) } func sizeUploadFileType(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) var t models.UploadFileType if db.First(&t, id).Error != nil { return } t.MaxSize = mbToBytes(c.PostForm("max_size")) db.Save(&t) } func deleteUploadFileType(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) db.Delete(&models.UploadFileType{}, id) } // ---------------- Download settings ---------------- // DownloadSettingsPage renders the download base-URL management page. func DownloadSettingsPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) var urls []models.DownloadBaseURL db.Order("is_default desc, priority asc, id asc").Find(&urls) data := DefaultData(c) data["Title"] = tr["settings_download_title"] data["BaseURLs"] = urls if msg := c.Query("saved"); msg == "1" { data["Success"] = tr["settings_saved"] } c.HTML(http.StatusOK, "settings_download", data) } } // DownloadSettingsSave dispatches download base-URL actions. func DownloadSettingsSave(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { switch c.PostForm("action") { case "add": addDownloadBaseURL(db, c) case "toggle": toggleDownloadBaseURL(db, c) case "default": defaultDownloadBaseURL(db, c) case "delete": deleteDownloadBaseURL(db, c) } models.RefreshConfigCache(db) c.Redirect(http.StatusFound, "/admin/settings/download?saved=1") } } func addDownloadBaseURL(db *gorm.DB, c *gin.Context) { name := strings.TrimSpace(c.PostForm("name")) base := strings.TrimSpace(c.PostForm("base_url")) if base == "" { return } prio, _ := strconv.Atoi(c.PostForm("priority")) b := models.DownloadBaseURL{ Name: name, BaseURL: base, Priority: prio, Enabled: c.PostForm("enabled") != "0", } db.Create(&b) } func toggleDownloadBaseURL(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) var b models.DownloadBaseURL if db.First(&b, id).Error != nil { return } b.Enabled = !b.Enabled db.Save(&b) } func defaultDownloadBaseURL(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) // Only one default at a time. db.Model(&models.DownloadBaseURL{}).Where("1=1").Update("is_default", false) db.Model(&models.DownloadBaseURL{}).Where("id = ?", id).Update("is_default", true) } func deleteDownloadBaseURL(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) db.Delete(&models.DownloadBaseURL{}, id) } // ---------------- Comment settings ---------------- // CommentSettingsPage renders the comment policy form. func CommentSettingsPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) var cc models.CommentConfig if err := db.First(&cc, 1).Error; err != nil { cc = *models.GetCommentConfig() } data := DefaultData(c) data["Title"] = tr["comment_settings_title"] data["CommentConfig"] = cc if msg := c.Query("saved"); msg == "1" { data["Success"] = tr["settings_saved"] } c.HTML(http.StatusOK, "settings_comment", data) } } // CommentSettingsSave persists the comment policy toggles and refreshes the // in-memory cache so subsequent requests see the change. func CommentSettingsSave(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var cc models.CommentConfig if err := db.First(&cc, 1).Error; err != nil { cc = models.CommentConfig{ID: 1} } cc.Enabled = c.PostForm("enabled") == "1" cc.AllowGuest = c.PostForm("allow_guest") == "1" cc.GuestRequireApproval = c.PostForm("guest_require_approval") == "1" cc.UseGravatar = c.PostForm("use_gravatar") == "1" cc.UpdatedBy = userIDFromSession(c) db.Save(&cc) models.RefreshConfigCache(db) c.Redirect(http.StatusFound, "/admin/settings/comments?saved=1") } } // ---------------- Navigation Links settings ---------------- // NavLinksSettingsPage renders the navigation links management page. func NavLinksSettingsPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) var links []models.NavLink db.Order("sort asc, id asc").Find(&links) data := DefaultData(c) data["Title"] = tr["settings_navlinks_title"] data["NavLinks"] = links if msg := c.Query("saved"); msg == "1" { data["Success"] = tr["settings_saved"] } c.HTML(http.StatusOK, "settings_navlinks", data) } } // NavLinksSettingsSave dispatches navigation link actions. func NavLinksSettingsSave(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { switch c.PostForm("action") { case "add": addNavLink(db, c) case "toggle": toggleNavLink(db, c) case "edit": editNavLink(db, c) case "delete": deleteNavLink(db, c) } models.RefreshConfigCache(db) c.Redirect(http.StatusFound, "/admin/settings/navlinks?saved=1") } } func addNavLink(db *gorm.DB, c *gin.Context) { titleZh := strings.TrimSpace(c.PostForm("title_zh")) titleEn := strings.TrimSpace(c.PostForm("title_en")) url := strings.TrimSpace(c.PostForm("url")) if url == "" || (titleZh == "" && titleEn == "") { return } sort, _ := strconv.Atoi(c.PostForm("sort")) link := models.NavLink{ TitleZh: titleZh, TitleEn: titleEn, URL: url, OpenNew: c.PostForm("open_new") == "1", Enabled: c.PostForm("enabled") != "0", Sort: sort, UpdatedBy: userIDFromSession(c), } db.Create(&link) } func toggleNavLink(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) var link models.NavLink if db.First(&link, id).Error != nil { return } link.Enabled = !link.Enabled link.UpdatedBy = userIDFromSession(c) db.Save(&link) } func editNavLink(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) var link models.NavLink if db.First(&link, id).Error != nil { return } titleZh := strings.TrimSpace(c.PostForm("title_zh")) titleEn := strings.TrimSpace(c.PostForm("title_en")) url := strings.TrimSpace(c.PostForm("url")) if url == "" || (titleZh == "" && titleEn == "") { return } link.TitleZh = titleZh link.TitleEn = titleEn link.URL = url link.OpenNew = c.PostForm("open_new") == "1" link.Sort, _ = strconv.Atoi(c.PostForm("sort")) link.UpdatedBy = userIDFromSession(c) db.Save(&link) } func deleteNavLink(db *gorm.DB, c *gin.Context) { id, _ := strconv.Atoi(c.PostForm("id")) db.Delete(&models.NavLink{}, id) }