settings.go
⎇
Raw
1package handler
2
3import (
4 "fmt"
5 "log/slog"
6 "net/http"
7 "net/url"
8 "strconv"
9 "strings"
10
11 "vidarchive/internal/models"
12)
13
14func (h *Handler) Settings(w http.ResponseWriter, r *http.Request) {
15 presets, err := h.presetSvc.GetAll()
16 if err != nil {
17 h.serverError(w, r, "list presets", err)
18 return
19 }
20
21 settings, err := h.settingsSvc.GetAll()
22 if err != nil {
23 h.serverError(w, r, "load settings", err)
24 return
25 }
26
27 h.renderWithRequest(w, r, "settings", PageData{
28 Title: "Settings",
29 ActiveTab: "settings",
30 Data: struct {
31 Presets []*models.Preset
32 Settings *models.Settings
33 }{
34 Presets: presets,
35 Settings: settings,
36 },
37 })
38}
39
40func (h *Handler) CreatePreset(w http.ResponseWriter, r *http.Request) {
41 if !parseForm(w, r) {
42 return
43 }
44
45 preset := &models.Preset{}
46 if err := applyPresetForm(preset, r); err != nil {
47 redirectWithError(w, r, "/settings", err.Error(), nil)
48 return
49 }
50
51 if err := h.presetSvc.Save(preset); err != nil {
52 redirectWithError(w, r, "/settings", "Couldn't create this preset.", err)
53 return
54 }
55
56 redirectWithSuccess(w, r, "/settings", "Preset created.")
57}
58
59// applyPresetForm is shared by create and update, so the two cannot drift apart
60// as fields are added.
61func applyPresetForm(p *models.Preset, r *http.Request) error {
62 name := strings.TrimSpace(r.FormValue("name"))
63 if name == "" {
64 return fmt.Errorf("A preset needs a name.")
65 }
66
67 // Mirrors the settings form's radio options. Empty means unspecified, and
68 // BuildArgs applies its own default.
69 formatMode := r.FormValue("format_mode")
70 switch formatMode {
71 case "", "default", "preset", "custom":
72 default:
73 return fmt.Errorf("Unknown format mode %q.", formatMode)
74 }
75
76 p.Name = name
77 p.Description = r.FormValue("description")
78 p.FormatMode = formatMode
79 p.Format = r.FormValue("format")
80 p.Quality = r.FormValue("quality")
81 p.CustomFormat = r.FormValue("custom_format")
82 p.AudioFormat = r.FormValue("audio_format")
83 p.SubLangs = r.FormValue("sub_langs")
84 p.CustomFlags = r.FormValue("custom_flags")
85 p.IsDefault = r.FormValue("is_default") == "1"
86 p.ExtractAudio = r.FormValue("extract_audio") == "1"
87 p.EmbedSubs = r.FormValue("embed_subs") == "1"
88 p.EmbedThumbnail = r.FormValue("embed_thumbnail") == "1"
89 p.EmbedMetadata = r.FormValue("embed_metadata") == "1"
90 p.WriteInfoJSON = r.FormValue("write_info_json") == "1"
91 p.WriteComments = r.FormValue("write_comments") == "1"
92 p.CommentSort = strings.TrimSpace(r.FormValue("comment_sort"))
93 p.CommentExtractorArgs = strings.TrimSpace(r.FormValue("comment_extractor_args"))
94 p.MaxComments = 0
95 if raw := strings.TrimSpace(r.FormValue("max_comments")); raw != "" {
96 maxComments, err := strconv.Atoi(raw)
97 if err != nil || maxComments < 0 {
98 return fmt.Errorf("Max comments must be a non-negative number.")
99 }
100 p.MaxComments = maxComments
101 }
102 // Comments live in the info JSON sidecar, so the dependent options must stay
103 // consistent even for a client that submits the form without JavaScript.
104 if !p.WriteInfoJSON || !p.WriteComments {
105 p.WriteComments = false
106 p.CommentSort = ""
107 p.MaxComments = 0
108 p.CommentExtractorArgs = ""
109 }
110
111 return nil
112}
113
114func (h *Handler) UpdatePreset(w http.ResponseWriter, r *http.Request) {
115 id, ok := parseID(w, r)
116 if !ok {
117 return
118 }
119
120 if !parseForm(w, r) {
121 return
122 }
123
124 preset, err := h.presetSvc.GetByID(id)
125 if err != nil {
126 http.Error(w, "Not found", http.StatusNotFound)
127 return
128 }
129
130 if err := applyPresetForm(preset, r); err != nil {
131 redirectWithError(w, r, "/settings", err.Error(), nil)
132 return
133 }
134
135 if err := h.presetSvc.Save(preset); err != nil {
136 redirectWithError(w, r, "/settings", "Couldn't update this preset.", err)
137 return
138 }
139
140 redirectWithSuccess(w, r, "/settings", "Preset updated.")
141}
142
143func (h *Handler) DeletePreset(w http.ResponseWriter, r *http.Request) {
144 id, ok := parseID(w, r)
145 if !ok {
146 return
147 }
148
149 if err := h.presetSvc.Delete(id); err != nil {
150 redirectWithError(w, r, "/settings", "Couldn't delete this preset.", err)
151 return
152 }
153
154 redirectWithSuccess(w, r, "/settings", "Preset deleted.")
155}
156
157func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
158 if !parseForm(w, r) {
159 return
160 }
161
162 var firstErr error
163 record := func(err error) {
164 if err != nil && firstErr == nil {
165 firstErr = err
166 }
167 }
168
169 if interval := r.FormValue("refresh_interval"); interval != "" {
170 record(h.settingsSvc.SetRefreshInterval(interval))
171 }
172 record(h.settingsSvc.SetAutoRefreshLibrary(r.FormValue("auto_refresh_library") == "1"))
173 record(h.settingsSvc.SetAutoRefreshDownloads(r.FormValue("auto_refresh_downloads") == "1"))
174 record(h.settingsSvc.SetCookies(r.FormValue("cookies")))
175
176 if firstErr != nil {
177 slog.Error("failed to update settings", "err", firstErr)
178 flashError(w, "Some settings couldn't be saved: "+firstErr.Error())
179 } else {
180 flashSuccess(w, "Settings saved.")
181 }
182 http.Redirect(w, r, "/settings", http.StatusSeeOther)
183}
184
185func (h *Handler) Theme(w http.ResponseWriter, r *http.Request) {
186 if !parseForm(w, r) {
187 return
188 }
189
190 theme := r.FormValue("theme")
191 if theme == "" {
192 theme = "auto"
193 }
194
195 setCookie(w, "theme", theme)
196
197 http.Redirect(w, r, localReferer(r), http.StatusSeeOther)
198}
199
200// localReferer keeps only the path: Referer is attacker-controlled, so honouring
201// its host would make this route an open redirect.
202func localReferer(r *http.Request) string {
203 ref, err := url.Parse(r.Header.Get("Referer"))
204 if err != nil || !strings.HasPrefix(ref.Path, "/") {
205 return "/"
206 }
207 // Browsers read "//host" and "/\host" as protocol-relative URLs, so such a
208 // path still redirects off-site.
209 if strings.HasPrefix(ref.Path, "//") || strings.HasPrefix(ref.Path, `/\`) {
210 return "/"
211 }
212 if ref.RawQuery == "" {
213 return ref.Path
214 }
215 return ref.Path + "?" + ref.RawQuery
216}
217