1package ctrladmin 2 3import ( 4 "encoding/gob" 5 "fmt" 6 "html/template" 7 "log" 8 "net/http" 9 "net/url" 10 "path/filepath" 11 "strings" 12 "time" 13 14 "github.com/Masterminds/sprig" 15 "github.com/dustin/go-humanize" 16 "github.com/gorilla/securecookie" 17 "github.com/gorilla/sessions" 18 "github.com/oxtoacart/bpool" 19 "github.com/wader/gormstore" 20 21 "senan.xyz/g/gonic/db" 22 "senan.xyz/g/gonic/server/assets" 23 "senan.xyz/g/gonic/server/ctrlbase" 24 "senan.xyz/g/gonic/version" 25) 26 27type CtxKey int 28 29const ( 30 CtxUser CtxKey = iota 31 CtxSession 32) 33 34// extendFromPaths /extends/ the given template for every asset 35// with given prefix 36func extendFromPaths(b *template.Template, p string) *template.Template { 37 assets.PrefixDo(p, func(_ string, asset *assets.EmbeddedAsset) { 38 tmplStr := string(asset.Bytes) 39 b = template.Must(b.Parse(tmplStr)) 40 }) 41 return b 42} 43 44// extendFromPaths /clones/ the given template for every asset 45// with given prefix, extends it, and insert it into a new map 46func pagesFromPaths(b *template.Template, p string) map[string]*template.Template { 47 ret := map[string]*template.Template{} 48 assets.PrefixDo(p, func(path string, asset *assets.EmbeddedAsset) { 49 tmplKey := filepath.Base(path) 50 clone := template.Must(b.Clone()) 51 tmplStr := string(asset.Bytes) 52 ret[tmplKey] = template.Must(clone.Parse(tmplStr)) 53 }) 54 return ret 55} 56 57const ( 58 prefixPartials = "partials" 59 prefixLayouts = "layouts" 60 prefixPages = "pages" 61) 62 63func funcMap() template.FuncMap { 64 return template.FuncMap{ 65 "noCache": func(in string) string { 66 parsed, _ := url.Parse(in) 67 params := parsed.Query() 68 params.Set("v", version.VERSION) 69 parsed.RawQuery = params.Encode() 70 return parsed.String() 71 }, 72 "date": func(in time.Time) string { 73 return strings.ToLower(in.Format("Jan 02, 2006")) 74 }, 75 "dateHuman": humanize.Time, 76 } 77} 78 79type Controller struct { 80 *ctrlbase.Controller 81 buffPool *bpool.BufferPool 82 templates map[string]*template.Template 83 sessDB *gormstore.Store 84} 85 86func New(base *ctrlbase.Controller) *Controller { 87 sessionKey := []byte(base.DB.GetSetting("session_key")) 88 if len(sessionKey) == 0 { 89 sessionKey = securecookie.GenerateRandomKey(32) 90 base.DB.SetSetting("session_key", string(sessionKey)) 91 } 92 tmplBase := template. 93 New("layout"). 94 Funcs(sprig.FuncMap()). 95 Funcs(funcMap()). // static 96 Funcs(template.FuncMap{ // from base 97 "path": base.Path, 98 }) 99 tmplBase = extendFromPaths(tmplBase, prefixPartials) 100 tmplBase = extendFromPaths(tmplBase, prefixLayouts) 101 sessDB := gormstore.New(base.DB.DB, sessionKey) 102 sessDB.SessionOpts.HttpOnly = true 103 sessDB.SessionOpts.SameSite = http.SameSiteLaxMode 104 return &Controller{ 105 Controller: base, 106 buffPool: bpool.NewBufferPool(64), 107 templates: pagesFromPaths(tmplBase, prefixPages), 108 sessDB: sessDB, 109 } 110} 111 112type templateData struct { 113 // common 114 Flashes []interface{} 115 User *db.User 116 Version string 117 // home 118 AlbumCount int 119 ArtistCount int 120 TrackCount int 121 RequestRoot string 122 RecentFolders []*db.Album 123 AllUsers []*db.User 124 LastScanTime time.Time 125 IsScanning bool 126 Playlists []*db.Playlist 127 TranscodePreferences []*db.TranscodePreference 128 TranscodeProfiles []string 129 // 130 CurrentLastFMAPIKey string 131 CurrentLastFMAPISecret string 132 SelectedUser *db.User 133} 134 135type Response struct { 136 // code is 200 137 template string 138 data *templateData 139 // code is 303 140 redirect string 141 flashN []string // normal 142 flashW []string // warning 143 // code is >= 400 144 code int 145 err string 146} 147 148type handlerAdmin func(r *http.Request) *Response 149 150//nolint:gocognit 151func (c *Controller) H(h handlerAdmin) http.Handler { 152 // TODO: break this up a bit 153 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 154 resp := h(r) 155 session, ok := r.Context().Value(CtxSession).(*sessions.Session) 156 if ok { 157 sessAddFlashN(session, resp.flashN) 158 sessAddFlashW(session, resp.flashW) 159 if err := session.Save(r, w); err != nil { 160 http.Error(w, fmt.Sprintf("error saving session: %v", err), 500) 161 return 162 } 163 } 164 if resp.redirect != "" { 165 to := resp.redirect 166 if strings.HasPrefix(to, "/") { 167 to = c.Path(to) 168 } 169 http.Redirect(w, r, to, http.StatusSeeOther) 170 return 171 } 172 if resp.err != "" { 173 http.Error(w, resp.err, resp.code) 174 return 175 } 176 if resp.template == "" { 177 http.Error(w, "useless handler return", 500) 178 return 179 } 180 if resp.data == nil { 181 resp.data = &templateData{} 182 } 183 resp.data.Version = version.VERSION 184 if session != nil { 185 resp.data.Flashes = session.Flashes() 186 if err := session.Save(r, w); err != nil { 187 http.Error(w, fmt.Sprintf("error saving session: %v", err), 500) 188 return 189 } 190 } 191 if user, ok := r.Context().Value(CtxUser).(*db.User); ok { 192 resp.data.User = user 193 } 194 buff := c.buffPool.Get() 195 defer c.buffPool.Put(buff) 196 tmpl, ok := c.templates[resp.template] 197 if !ok { 198 http.Error(w, fmt.Sprintf("finding template %q", resp.template), 500) 199 return 200 } 201 if err := tmpl.Execute(buff, resp.data); err != nil { 202 http.Error(w, fmt.Sprintf("executing template: %v", err), 500) 203 return 204 } 205 w.Header().Set("Content-Type", "text/html; charset=utf-8") 206 if resp.code != 0 { 207 w.WriteHeader(resp.code) 208 } 209 if _, err := buff.WriteTo(w); err != nil { 210 log.Printf("error writing to response buffer: %v\n", err) 211 } 212 }) 213} 214 215// ## begin utilities 216// ## begin utilities 217// ## begin utilities 218 219type FlashType string 220 221const ( 222 FlashNormal = FlashType("normal") 223 FlashWarning = FlashType("warning") 224) 225 226type Flash struct { 227 Message string 228 Type FlashType 229} 230 231func init() { 232 gob.Register(&Flash{}) 233} 234 235func sessAddFlashN(s *sessions.Session, messages []string) { 236 sessAddFlash(s, messages, FlashNormal) 237} 238 239func sessAddFlashW(s *sessions.Session, messages []string) { 240 sessAddFlash(s, messages, FlashWarning) 241} 242 243func sessAddFlash(s *sessions.Session, messages []string, flashT FlashType) { 244 if len(messages) == 0 { 245 return 246 } 247 for i, message := range messages { 248 if i > 6 { 249 break 250 } 251 s.AddFlash(Flash{ 252 Message: message, 253 Type: flashT, 254 }) 255 } 256} 257 258func sessLogSave(s *sessions.Session, w http.ResponseWriter, r *http.Request) { 259 if err := s.Save(r, w); err != nil { 260 log.Printf("error saving session: %v\n", err) 261 } 262} 263 264// ## begin validation 265// ## begin validation 266// ## begin validation 267 268func validateUsername(username string) error { 269 if username == "" { 270 return fmt.Errorf("please enter the username") 271 } 272 return nil 273} 274 275func validatePasswords(pOne, pTwo string) error { 276 if pOne == "" || pTwo == "" { 277 return fmt.Errorf("please enter the password twice") 278 } 279 if !(pOne == pTwo) { 280 return fmt.Errorf("the two passwords entered were not the same") 281 } 282 return nil 283} 284 285func validateAPIKey(apiKey, secret string) error { 286 if apiKey == "" || secret == "" { 287 return fmt.Errorf("please enter both the api key and secret") 288 } 289 return nil 290} 291