package common import ( "database/sql" "encoding/json" "errors" "io" "io/ioutil" "log" "net/http" "os" "reflect" "sync" "sync/atomic" "github.com/Azareal/Gosora/query_gen" ) // TODO: Something more thread-safe type ThemeList map[string]*Theme var Themes ThemeList = make(map[string]*Theme) // ? Refactor this into a store? var DefaultThemeBox atomic.Value var ChangeDefaultThemeMutex sync.Mutex // TODO: Fallback to a random theme if this doesn't exist, so admins can remove themes they don't use // TODO: Use this when the default theme doesn't exist var fallbackTheme = "cosora" var overridenTemplates = make(map[string]bool) // ? What is this used for? type ThemeStmts struct { getThemes *sql.Stmt isThemeDefault *sql.Stmt updateTheme *sql.Stmt addTheme *sql.Stmt } var themeStmts ThemeStmts func init() { DefaultThemeBox.Store(fallbackTheme) DbInits.Add(func(acc *qgen.Accumulator) error { themeStmts = ThemeStmts{ getThemes: acc.Select("themes").Columns("uname, default").Prepare(), isThemeDefault: acc.Select("themes").Columns("default").Where("uname = ?").Prepare(), updateTheme: acc.Update("themes").Set("default = ?").Where("uname = ?").Prepare(), addTheme: acc.Insert("themes").Columns("uname, default").Fields("?,?").Prepare(), } return acc.FirstError() }) } func NewThemeList() (themes ThemeList, err error) { themes = make(map[string]*Theme) themeFiles, err := ioutil.ReadDir("./themes") if err != nil { return themes, err } for _, themeFile := range themeFiles { if !themeFile.IsDir() { continue } themeName := themeFile.Name() log.Printf("Adding theme '%s'", themeName) themePath := "./themes/" + themeName themeFile, err := ioutil.ReadFile(themePath + "/theme.json") if err != nil { return themes, err } var theme = &Theme{Name: ""} err = json.Unmarshal(themeFile, theme) if err != nil { return themes, err } // TODO: Implement the static file part of this and fsnotify if theme.Path != "" { log.Print("Resolving redirect to " + theme.Path) themeFile, err := ioutil.ReadFile(theme.Path + "/theme.json") if err != nil { return themes, err } theme = &Theme{Name: "", Path: theme.Path} err = json.Unmarshal(themeFile, theme) if err != nil { return themes, err } } else { theme.Path = themePath } theme.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file // TODO: Let the theme specify where it's resources are via the JSON file? // TODO: Let the theme inherit CSS from another theme? // ? - This might not be too helpful, as it only searches for /public/ and not if /public/ is empty. Still, it might help some people with a slightly less cryptic error log.Print(theme.Path + "/public/") _, err = os.Stat(theme.Path + "/public/") if err != nil { if os.IsNotExist(err) { return themes, errors.New("We couldn't find this theme's resources. E.g. the /public/ folder.") } else { log.Print("We weren't able to access this theme's resources due to a permissions issue or some other problem") return themes, err } } if theme.FullImage != "" { DebugLog("Adding theme image") err = StaticFiles.Add(theme.Path+"/"+theme.FullImage, themePath) if err != nil { return themes, err } } theme.TemplatesMap = make(map[string]string) theme.TmplPtr = make(map[string]interface{}) if theme.Templates != nil { for _, themeTmpl := range theme.Templates { theme.TemplatesMap[themeTmpl.Name] = themeTmpl.Source theme.TmplPtr[themeTmpl.Name] = TmplPtrMap["o_"+themeTmpl.Source] } } // TODO: Bind the built template, or an interpreted one for any dock overrides this theme has themes[theme.Name] = theme } return themes, nil } // TODO: Make the initThemes and LoadThemes functions less confusing // ? - Delete themes which no longer exist in the themes folder from the database? func (themes ThemeList) LoadActiveStatus() error { ChangeDefaultThemeMutex.Lock() defer ChangeDefaultThemeMutex.Unlock() rows, err := themeStmts.getThemes.Query() if err != nil { return err } defer rows.Close() var uname string var defaultThemeSwitch bool for rows.Next() { err = rows.Scan(&uname, &defaultThemeSwitch) if err != nil { return err } // Was the theme deleted at some point? theme, ok := themes[uname] if !ok { continue } if defaultThemeSwitch { DebugLogf("Loading the default theme '%s'", theme.Name) theme.Active = true DefaultThemeBox.Store(theme.Name) theme.MapTemplates() } else { DebugLogf("Loading the theme '%s'", theme.Name) theme.Active = false } themes[uname] = theme } return rows.Err() } func (themes ThemeList) LoadStaticFiles() error { for _, theme := range themes { err := theme.LoadStaticFiles() if err != nil { return err } } return nil } func ResetTemplateOverrides() { log.Print("Resetting the template overrides") for name := range overridenTemplates { log.Print("Resetting '" + name + "' template override") originPointer, ok := TmplPtrMap["o_"+name] if !ok { log.Print("The origin template doesn't exist!") return } destTmplPtr, ok := TmplPtrMap[name] if !ok { log.Print("The destination template doesn't exist!") return } // Not really a pointer, more of a function handle, an artifact from one of the earlier versions of themes.go switch oPtr := originPointer.(type) { case func(CustomPagePage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(CustomPagePage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(TopicPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(TopicPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(TopicListPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(TopicListPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(ForumPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(ForumPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(ForumsPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(ForumsPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(ProfilePage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(ProfilePage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(CreateTopicPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(CreateTopicPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(IPSearchPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(IPSearchPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(AccountDashPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(AccountDashPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(ErrorPage, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(ErrorPage, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } case func(Page, io.Writer) error: switch dPtr := destTmplPtr.(type) { case *func(Page, io.Writer) error: *dPtr = oPtr default: LogError(errors.New("The source and destination templates are incompatible")) } default: log.Print("name: ", name) LogError(errors.New("Unknown destination template type!")) } log.Print("The template override was reset") } overridenTemplates = make(map[string]bool) log.Print("All of the template overrides have been reset") } type GzipResponseWriter struct { io.Writer http.ResponseWriter } func (w GzipResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) } // NEW method of doing theme templates to allow one user to have a different theme to another. Under construction. // TODO: Generate the type switch instead of writing it by hand // TODO: Cut the number of types in half func (theme *Theme) RunTmpl(template string, pi interface{}, w io.Writer) error { // Unpack this to avoid an indirect call gzw, ok := w.(GzipResponseWriter) if ok { w = gzw.Writer } var getTmpl = theme.GetTmpl(template) switch tmplO := getTmpl.(type) { case *func(CustomPagePage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(CustomPagePage), w) case *func(TopicPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(TopicPage), w) case *func(TopicListPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(TopicListPage), w) case *func(ForumPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(ForumPage), w) case *func(ForumsPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(ForumsPage), w) case *func(ProfilePage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(ProfilePage), w) case *func(CreateTopicPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(CreateTopicPage), w) case *func(IPSearchPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(IPSearchPage), w) case *func(AccountDashPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(AccountDashPage), w) case *func(ErrorPage, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(ErrorPage), w) case *func(Page, io.Writer) error: var tmpl = *tmplO return tmpl(pi.(Page), w) case func(CustomPagePage, io.Writer) error: return tmplO(pi.(CustomPagePage), w) case func(TopicPage, io.Writer) error: return tmplO(pi.(TopicPage), w) case func(TopicListPage, io.Writer) error: return tmplO(pi.(TopicListPage), w) case func(ForumPage, io.Writer) error: return tmplO(pi.(ForumPage), w) case func(ForumsPage, io.Writer) error: return tmplO(pi.(ForumsPage), w) case func(ProfilePage, io.Writer) error: return tmplO(pi.(ProfilePage), w) case func(CreateTopicPage, io.Writer) error: return tmplO(pi.(CreateTopicPage), w) case func(IPSearchPage, io.Writer) error: return tmplO(pi.(IPSearchPage), w) case func(AccountDashPage, io.Writer) error: return tmplO(pi.(AccountDashPage), w) case func(ErrorPage, io.Writer) error: return tmplO(pi.(ErrorPage), w) case func(Page, io.Writer) error: return tmplO(pi.(Page), w) case nil, string: mapping, ok := theme.TemplatesMap[template] if !ok { mapping = template } return Templates.ExecuteTemplate(w, mapping+".html", pi) default: log.Print("theme ", theme) log.Print("template ", template) log.Print("pi ", pi) log.Print("tmplO ", tmplO) log.Print("getTmpl ", getTmpl) valueOf := reflect.ValueOf(tmplO) log.Print("initial valueOf.Type()", valueOf.Type()) for valueOf.Kind() == reflect.Interface || valueOf.Kind() == reflect.Ptr { valueOf = valueOf.Elem() log.Print("valueOf.Elem().Type() ", valueOf.Type()) } log.Print("deferenced valueOf.Type() ", valueOf.Type()) log.Print("valueOf.Kind() ", valueOf.Kind()) return errors.New("Unknown template type") } } // GetThemeTemplate attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter func (theme *Theme) GetTmpl(template string) interface{} { // TODO: Figure out why we're getting a nil pointer here when transpiled templates are disabled, I would have assumed that we would just fall back to !ok on this // Might have something to do with it being the theme's TmplPtr map, investigate. tmpl, ok := theme.TmplPtr[template] if ok { return tmpl } tmpl, ok = TmplPtrMap[template] if ok { return tmpl } return template } // CreateThemeTemplate creates a theme template on the current default theme func CreateThemeTemplate(theme string, name string) { Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error { mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[name] if !ok { mapping = name } return Templates.ExecuteTemplate(w, mapping+".html", pi) } } func GetDefaultThemeName() string { return DefaultThemeBox.Load().(string) } func SetDefaultThemeName(name string) { DefaultThemeBox.Store(name) }