settings.go
⎇
Raw
1package handler
2
3import (
4 "fmt"
5 "log"
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 err := r.ParseForm(); err != nil {
42 http.Error(w, err.Error(), http.StatusBadRequest)
43 return
44 }
45
46 preset := &models.Preset{}
47 if err := applyPresetForm(preset, r); err != nil {
48 redirectWithError(w, r, "/settings", err.Error(), nil)
49 return
50 }
51
52 if err := h.presetSvc.Save(preset); err != nil {
53 redirectWithError(w, r, "/settings", "Couldn't create this preset.", err)
54 return
55 }
56
57 redirectWithSuccess(w, r, "/settings", "Preset created.")
58}
59
60// applyPresetForm copies the preset form fields onto p and validates them. It is
61// shared by create and update so the two can't drift apart as fields are added.
62func applyPresetForm(p *models.Preset, r *http.Request) error {
63 name := strings.TrimSpace(r.FormValue("name"))
64 if name == "" {
65 return fmt.Errorf("A preset needs a name.")
66 }
67
68 // Mirrors the radio options on the settings form; empty means "unspecified"
69 // and BuildArgs applies its own default.
70 formatMode := r.FormValue("format_mode")
71 switch formatMode {
72 case "", "default", "preset", "custom":
73 default:
74 return fmt.Errorf("Unknown format mode %q.", formatMode)
75 }
76
77 p.Name = name
78 p.Description = r.FormValue("description")
79 p.FormatMode = formatMode
80 p.Format = r.FormValue("format")
81 p.Quality = r.FormValue("quality")
82 p.CustomFormat = r.FormValue("custom_format")
83 p.AudioFormat = r.FormValue("audio_format")
84 p.SubLangs = r.FormValue("sub_langs")
85 p.CustomFlags = r.FormValue("custom_flags")
86 p.IsDefault = r.FormValue("is_default") == "1"
87 p.ExtractAudio = r.FormValue("extract_audio") == "1"
88 p.EmbedSubs = r.FormValue("embed_subs") == "1"
89 p.EmbedThumbnail = r.FormValue("embed_thumbnail") == "1"
90 p.EmbedMetadata = r.FormValue("embed_metadata") == "1"
91 p.WriteInfoJSON = r.FormValue("write_info_json") == "1"
92 p.WriteComments = r.FormValue("write_comments") == "1"
93 p.CommentSort = strings.TrimSpace(r.FormValue("comment_sort"))
94 p.CommentExtractorArgs = strings.TrimSpace(r.FormValue("comment_extractor_args"))
95 p.MaxComments = 0
96 if raw := strings.TrimSpace(r.FormValue("max_comments")); raw != "" {
97 maxComments, err := strconv.Atoi(raw)
98 if err != nil || maxComments < 0 {
99 return fmt.Errorf("Max comments must be a non-negative number.")
100 }
101 p.MaxComments = maxComments
102 }
103 // Comments are stored in the info JSON sidecar. Keep the dependent options
104 // consistent even when a client submits the form without JavaScript.
105 if !p.WriteInfoJSON || !p.WriteComments {
106 p.WriteComments = false
107 p.CommentSort = ""
108 p.MaxComments = 0
109 p.CommentExtractorArgs = ""
110 }
111
112 return nil
113}
114
115func (h *Handler) UpdatePreset(w http.ResponseWriter, r *http.Request) {
116 id, ok := parseID(w, r)
117 if !ok {
118 return
119 }
120
121 if err := r.ParseForm(); err != nil {
122 http.Error(w, err.Error(), http.StatusBadRequest)
123 return
124 }
125
126 preset, err := h.presetSvc.GetByID(id)
127 if err != nil {
128 http.Error(w, "Not found", http.StatusNotFound)
129 return
130 }
131
132 if err := applyPresetForm(preset, r); err != nil {
133 redirectWithError(w, r, "/settings", err.Error(), nil)
134 return
135 }
136
137 if err := h.presetSvc.Save(preset); err != nil {
138 redirectWithError(w, r, "/settings", "Couldn't update this preset.", err)
139 return
140 }
141
142 redirectWithSuccess(w, r, "/settings", "Preset updated.")
143}
144
145func (h *Handler) DeletePreset(w http.ResponseWriter, r *http.Request) {
146 id, ok := parseID(w, r)
147 if !ok {
148 return
149 }
150
151 if err := h.presetSvc.Delete(id); err != nil {
152 redirectWithError(w, r, "/settings", "Couldn't delete this preset.", err)
153 return
154 }
155
156 redirectWithSuccess(w, r, "/settings", "Preset deleted.")
157}
158
159func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
160 if err := r.ParseForm(); err != nil {
161 http.Error(w, err.Error(), http.StatusBadRequest)
162 return
163 }
164
165 var firstErr error
166 record := func(err error) {
167 if err != nil && firstErr == nil {
168 firstErr = err
169 }
170 }
171
172 if interval := r.FormValue("refresh_interval"); interval != "" {
173 record(h.settingsSvc.SetRefreshInterval(interval))
174 }
175 record(h.settingsSvc.SetAutoRefreshLibrary(r.FormValue("auto_refresh_library") == "1"))
176 record(h.settingsSvc.SetAutoRefreshDownloads(r.FormValue("auto_refresh_downloads") == "1"))
177 record(h.settingsSvc.SetCookies(r.FormValue("cookies")))
178
179 if firstErr != nil {
180 log.Printf("UpdateSettings: %v", firstErr)
181 flashError(w, "Some settings couldn't be saved: "+firstErr.Error())
182 } else {
183 flashSuccess(w, "Settings saved.")
184 }
185 http.Redirect(w, r, "/settings", http.StatusSeeOther)
186}
187
188func (h *Handler) Theme(w http.ResponseWriter, r *http.Request) {
189 if err := r.ParseForm(); err != nil {
190 http.Error(w, err.Error(), http.StatusBadRequest)
191 return
192 }
193
194 theme := r.FormValue("theme")
195 if theme == "" {
196 theme = "auto"
197 }
198
199 setCookie(w, "theme", theme)
200
201 http.Redirect(w, r, localReferer(r), http.StatusSeeOther)
202}
203
204// localReferer returns where to send the user back to. Only the path is kept:
205// Referer is attacker-controlled, so honouring its host would make this route
206// an open redirect.
207func localReferer(r *http.Request) string {
208 ref, err := url.Parse(r.Header.Get("Referer"))
209 if err != nil || !strings.HasPrefix(ref.Path, "/") {
210 return "/"
211 }
212 // "//host" and "/\host" are protocol-relative URLs to browsers, so a path
213 // starting with them would redirect off-site. Only a single leading slash
214 // followed by a normal path segment stays local.
215 if strings.HasPrefix(ref.Path, "//") || strings.HasPrefix(ref.Path, `/\`) {
216 return "/"
217 }
218 if ref.RawQuery == "" {
219 return ref.Path
220 }
221 return ref.Path + "?" + ref.RawQuery
222}
223