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  }