github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/theme_list.go (about) 1 package common 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "errors" 7 "html/template" 8 "io" 9 "io/ioutil" 10 "log" 11 "net/http" 12 "os" 13 "path/filepath" 14 "strings" 15 "sync" 16 "sync/atomic" 17 18 qgen "github.com/Azareal/Gosora/query_gen" 19 ) 20 21 // TODO: Something more thread-safe 22 type ThemeList map[string]*Theme 23 24 var Themes ThemeList = make(map[string]*Theme) // ? Refactor this into a store? 25 var DefaultThemeBox atomic.Value 26 var ChangeDefaultThemeMutex sync.Mutex 27 var ThemesSlice []*Theme 28 29 // TODO: Fallback to a random theme if this doesn't exist, so admins can remove themes they don't use 30 // TODO: Use this when the default theme doesn't exist 31 var fallbackTheme = "cosora" 32 var overridenTemplates = make(map[string]bool) // ? What is this used for? 33 34 type ThemeStmts struct { 35 getAll *sql.Stmt 36 isDefault *sql.Stmt 37 update *sql.Stmt 38 add *sql.Stmt 39 } 40 41 var themeStmts ThemeStmts 42 43 func init() { 44 DbInits.Add(func(acc *qgen.Accumulator) error { 45 t, cols := "themes", "uname,default" 46 themeStmts = ThemeStmts{ 47 getAll: acc.Select(t).Columns(cols).Prepare(), 48 isDefault: acc.Select(t).Columns("default").Where("uname=?").Prepare(), 49 update: acc.Update(t).Set("default=?").Where("uname=?").Prepare(), 50 add: acc.Insert(t).Columns(cols).Fields("?,?").Prepare(), 51 } 52 return acc.FirstError() 53 }) 54 } 55 56 func NewThemeList() (themes ThemeList, err error) { 57 themes = make(map[string]*Theme) 58 themeFiles, err := ioutil.ReadDir("./themes") 59 if err != nil { 60 return themes, err 61 } 62 if len(themeFiles) == 0 { 63 return themes, errors.New("You don't have any themes") 64 } 65 66 var lastTheme, defaultTheme string 67 for _, themeFile := range themeFiles { 68 if !themeFile.IsDir() { 69 continue 70 } 71 72 themeName := themeFile.Name() 73 log.Printf("Adding theme '%s'", themeName) 74 themePath := "./themes/" + themeName 75 themeFile, err := ioutil.ReadFile(themePath + "/theme.json") 76 if err != nil { 77 return themes, err 78 } 79 80 th := &Theme{} 81 err = json.Unmarshal(themeFile, th) 82 if err != nil { 83 return themes, err 84 } 85 86 if th.Name == "" { 87 return themes, errors.New("Theme " + themePath + " doesn't have a name set in theme.json") 88 } 89 if th.Name == fallbackTheme { 90 defaultTheme = fallbackTheme 91 } 92 lastTheme = th.Name 93 94 // TODO: Implement the static file part of this and fsnotify 95 if th.Path != "" { 96 log.Print("Resolving redirect to " + th.Path) 97 themeFile, err := ioutil.ReadFile(th.Path + "/theme.json") 98 if err != nil { 99 return themes, err 100 } 101 th = &Theme{Path: th.Path} 102 err = json.Unmarshal(themeFile, th) 103 if err != nil { 104 return themes, err 105 } 106 } else { 107 th.Path = themePath 108 } 109 110 th.Active = false // Set this to false, just in case someone explicitly overrode this value in the JSON file 111 112 // TODO: Let the theme specify where it's resources are via the JSON file? 113 // TODO: Let the theme inherit CSS from another theme? 114 // ? - 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 115 log.Print(th.Path + "/public/") 116 _, err = os.Stat(th.Path + "/public/") 117 if err != nil { 118 if os.IsNotExist(err) { 119 return themes, errors.New("We couldn't find this theme's resources. E.g. the /public/ folder.") 120 } else { 121 log.Print("We weren't able to access this theme's resources due to a permissions issue or some other problem") 122 return themes, err 123 } 124 } 125 126 if th.FullImage != "" { 127 DebugLog("Adding theme image") 128 err = StaticFiles.Add(th.Path+"/"+th.FullImage, themePath) 129 if err != nil { 130 return themes, err 131 } 132 } 133 134 th.TemplatesMap = make(map[string]string) 135 th.TmplPtr = make(map[string]interface{}) 136 if th.Templates != nil { 137 for _, themeTmpl := range th.Templates { 138 th.TemplatesMap[themeTmpl.Name] = themeTmpl.Source 139 th.TmplPtr[themeTmpl.Name] = TmplPtrMap["o_"+themeTmpl.Source] 140 } 141 } 142 143 th.IntTmplHandle = DefaultTemplates 144 overrides, err := ioutil.ReadDir(th.Path + "/overrides/") 145 if err != nil && !os.IsNotExist(err) { 146 return themes, err 147 } 148 if len(overrides) > 0 { 149 overCount := 0 150 th.OverridenMap = make(map[string]bool) 151 for _, override := range overrides { 152 if override.IsDir() { 153 continue 154 } 155 ext := filepath.Ext(themePath + "/overrides/" + override.Name()) 156 log.Print("attempting to add " + themePath + "/overrides/" + override.Name()) 157 if ext != ".html" { 158 log.Print("not a html file") 159 continue 160 } 161 overCount++ 162 nosuf := strings.TrimSuffix(override.Name(), ext) 163 th.OverridenTemplates = append(th.OverridenTemplates, nosuf) 164 th.OverridenMap[nosuf] = true 165 //th.TmplPtr[nosuf] = TmplPtrMap["o_"+nosuf] 166 log.Print("succeeded") 167 } 168 169 localTmpls := template.New("") 170 err = loadTemplates(localTmpls, th.Name) 171 if err != nil { 172 return themes, err 173 } 174 th.IntTmplHandle = localTmpls 175 log.Printf("theme.OverridenTemplates: %+v\n", th.OverridenTemplates) 176 log.Printf("theme.IntTmplHandle: %+v\n", th.IntTmplHandle) 177 } else { 178 log.Print("no overrides for " + th.Name) 179 } 180 181 for i, res := range th.Resources { 182 ext := filepath.Ext(res.Name) 183 switch ext { 184 case ".css": 185 res.Type = ResTypeSheet 186 case ".js": 187 res.Type = ResTypeScript 188 } 189 switch res.Location { 190 case "global": 191 res.LocID = LocGlobal 192 case "frontend": 193 res.LocID = LocFront 194 case "panel": 195 res.LocID = LocPanel 196 } 197 th.Resources[i] = res 198 } 199 200 for _, dock := range th.Docks { 201 if id, ok := DockToID[dock]; ok { 202 th.DocksID = append(th.DocksID, id) 203 } 204 } 205 206 // TODO: Bind the built template, or an interpreted one for any dock overrides this theme has 207 208 themes[th.Name] = th 209 ThemesSlice = append(ThemesSlice, th) 210 } 211 if defaultTheme == "" { 212 defaultTheme = lastTheme 213 } 214 DefaultThemeBox.Store(defaultTheme) 215 216 return themes, nil 217 } 218 219 // TODO: Make the initThemes and LoadThemes functions less confusing 220 // ? - Delete themes which no longer exist in the themes folder from the database? 221 func (t ThemeList) LoadActiveStatus() error { 222 ChangeDefaultThemeMutex.Lock() 223 defer ChangeDefaultThemeMutex.Unlock() 224 225 rows, e := themeStmts.getAll.Query() 226 if e != nil { 227 return e 228 } 229 defer rows.Close() 230 231 var uname string 232 var defaultThemeSwitch bool 233 for rows.Next() { 234 e = rows.Scan(&uname, &defaultThemeSwitch) 235 if e != nil { 236 return e 237 } 238 239 // Was the theme deleted at some point? 240 theme, ok := t[uname] 241 if !ok { 242 continue 243 } 244 245 if defaultThemeSwitch { 246 DebugLogf("Loading the default theme '%s'", theme.Name) 247 theme.Active = true 248 DefaultThemeBox.Store(theme.Name) 249 theme.MapTemplates() 250 } else { 251 DebugLogf("Loading the theme '%s'", theme.Name) 252 theme.Active = false 253 } 254 255 t[uname] = theme 256 } 257 return rows.Err() 258 } 259 260 func (t ThemeList) LoadStaticFiles() error { 261 for _, theme := range t { 262 if e := theme.LoadStaticFiles(); e != nil { 263 return e 264 } 265 } 266 return nil 267 } 268 269 func ResetTemplateOverrides() { 270 log.Print("Resetting the template overrides") 271 for name := range overridenTemplates { 272 log.Print("Resetting '" + name + "' template override") 273 originPointer, ok := TmplPtrMap["o_"+name] 274 if !ok { 275 log.Print("The origin template doesn't exist!") 276 return 277 } 278 destTmplPtr, ok := TmplPtrMap[name] 279 if !ok { 280 log.Print("The destination template doesn't exist!") 281 return 282 } 283 284 // Not really a pointer, more of a function handle, an artifact from one of the earlier versions of themes.go 285 oPtr, ok := originPointer.(func(interface{}, io.Writer) error) 286 if !ok { 287 log.Print("name: ", name) 288 LogError(errors.New("Unknown destination template type!")) 289 return 290 } 291 292 dPtr, ok := destTmplPtr.(*func(interface{}, io.Writer) error) 293 if !ok { 294 LogError(errors.New("The source and destination templates are incompatible")) 295 return 296 } 297 *dPtr = oPtr 298 log.Print("The template override was reset") 299 } 300 overridenTemplates = make(map[string]bool) 301 log.Print("All of the template overrides have been reset") 302 } 303 304 // CreateThemeTemplate creates a theme template on the current default theme 305 func CreateThemeTemplate(theme, name string) { 306 Themes[theme].TmplPtr[name] = func(pi Page, w http.ResponseWriter) error { 307 mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[name] 308 if !ok { 309 mapping = name 310 } 311 return DefaultTemplates.ExecuteTemplate(w, mapping+".html", pi) 312 } 313 } 314 315 func GetDefaultThemeName() string { 316 return DefaultThemeBox.Load().(string) 317 } 318 319 func SetDefaultThemeName(name string) { 320 DefaultThemeBox.Store(name) 321 }