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