a20078d83b
Revamped the Word Filter Manager for Nox. Revamped the Setting Manager for Nox and Cosora. Upped the number of items in the User Manager. Upped the number of items in the Group Manager. Upped the number of items in the Page Manager. Swap a fmt.Println for a DebugLog in hold.ScanItem. EQCSS.js should ignore panel.css in Cosora now. Added the lang template function for stylesheet templates to reduce the amount of boilerplate. Localised a couple of spots in the Nox Theme which got overlooked. Tweaked the grid CSS for Nox. The Control Panel Dashboard items now change colour in Nox like in the other themes. Use Site.Host instead of req.Host for www redirects for security reasons. Removed a superfluous function call in WriterIntercept.WriteHeader. Tweaked several bits and pieces of CSS like the padding on a few items in the Forum Editor. Added the topic_list.moderate phrase. Added the panel_word_filters_to phrase.
500 lines
14 KiB
Go
500 lines
14 KiB
Go
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Azareal/Gosora/common/phrases"
|
|
"github.com/Azareal/Gosora/query_gen"
|
|
)
|
|
|
|
type MenuItemList []MenuItem
|
|
|
|
type MenuListHolder struct {
|
|
MenuID int
|
|
List MenuItemList
|
|
Variations map[int]menuTmpl // 0 = Guest Menu, 1 = Member Menu, 2 = Super Mod Menu, 3 = Admin Menu
|
|
}
|
|
|
|
type menuPath struct {
|
|
Path string
|
|
Index int
|
|
}
|
|
|
|
type menuTmpl struct {
|
|
RenderBuffer [][]byte
|
|
VariableIndices []int
|
|
PathMappings []menuPath
|
|
}
|
|
|
|
type MenuItem struct {
|
|
ID int
|
|
MenuID int
|
|
|
|
Name string
|
|
HTMLID string
|
|
CSSClass string
|
|
Position string
|
|
Path string
|
|
Aria string
|
|
Tooltip string
|
|
Order int
|
|
TmplName string
|
|
|
|
GuestOnly bool
|
|
MemberOnly bool
|
|
SuperModOnly bool
|
|
AdminOnly bool
|
|
}
|
|
|
|
// TODO: Move the menu item stuff to it's own file
|
|
type MenuItemStmts struct {
|
|
update *sql.Stmt
|
|
insert *sql.Stmt
|
|
delete *sql.Stmt
|
|
updateOrder *sql.Stmt
|
|
}
|
|
|
|
var menuItemStmts MenuItemStmts
|
|
|
|
func init() {
|
|
DbInits.Add(func(acc *qgen.Accumulator) error {
|
|
menuItemStmts = MenuItemStmts{
|
|
update: acc.Update("menu_items").Set("name = ?, htmlID = ?, cssClass = ?, position = ?, path = ?, aria = ?, tooltip = ?, tmplName = ?, guestOnly = ?, memberOnly = ?, staffOnly = ?, adminOnly = ?").Where("miid = ?").Prepare(),
|
|
insert: acc.Insert("menu_items").Columns("mid, name, htmlID, cssClass, position, path, aria, tooltip, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Fields("?,?,?,?,?,?,?,?,?,?,?,?,?").Prepare(),
|
|
delete: acc.Delete("menu_items").Where("miid = ?").Prepare(),
|
|
updateOrder: acc.Update("menu_items").Set("order = ?").Where("miid = ?").Prepare(),
|
|
}
|
|
return acc.FirstError()
|
|
})
|
|
}
|
|
|
|
func (item MenuItem) Commit() error {
|
|
_, err := menuItemStmts.update.Exec(item.Name, item.HTMLID, item.CSSClass, item.Position, item.Path, item.Aria, item.Tooltip, item.TmplName, item.GuestOnly, item.MemberOnly, item.SuperModOnly, item.AdminOnly, item.ID)
|
|
Menus.Load(item.MenuID)
|
|
return err
|
|
}
|
|
|
|
func (item MenuItem) Create() (int, error) {
|
|
res, err := menuItemStmts.insert.Exec(item.MenuID, item.Name, item.HTMLID, item.CSSClass, item.Position, item.Path, item.Aria, item.Tooltip, item.TmplName, item.GuestOnly, item.MemberOnly, item.SuperModOnly, item.AdminOnly)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
Menus.Load(item.MenuID)
|
|
|
|
miid64, err := res.LastInsertId()
|
|
return int(miid64), err
|
|
}
|
|
|
|
func (item MenuItem) Delete() error {
|
|
_, err := menuItemStmts.delete.Exec(item.ID)
|
|
Menus.Load(item.MenuID)
|
|
return err
|
|
}
|
|
|
|
func (hold *MenuListHolder) LoadTmpl(name string) (menuTmpl MenuTmpl, err error) {
|
|
data, err := ioutil.ReadFile("./templates/" + name + ".html")
|
|
if err != nil {
|
|
return menuTmpl, err
|
|
}
|
|
return hold.Parse(name, data), nil
|
|
}
|
|
|
|
// TODO: Make this atomic, maybe with a transaction or store the order on the menu itself?
|
|
func (hold *MenuListHolder) UpdateOrder(updateMap map[int]int) error {
|
|
for miid, order := range updateMap {
|
|
_, err := menuItemStmts.updateOrder.Exec(order, miid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
Menus.Load(hold.MenuID)
|
|
return nil
|
|
}
|
|
|
|
func (hold *MenuListHolder) LoadTmpls() (tmpls map[string]MenuTmpl, err error) {
|
|
tmpls = make(map[string]MenuTmpl)
|
|
var loadTmpl = func(name string) error {
|
|
menuTmpl, err := hold.LoadTmpl(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmpls[name] = menuTmpl
|
|
return nil
|
|
}
|
|
err = loadTmpl("menu_item")
|
|
if err != nil {
|
|
return tmpls, err
|
|
}
|
|
err = loadTmpl("menu_alerts")
|
|
return tmpls, err
|
|
}
|
|
|
|
// TODO: Run this in main, sync ticks, when the phrase file changes (need to implement the sync for that first), and when the settings are changed
|
|
func (hold *MenuListHolder) Preparse() error {
|
|
tmpls, err := hold.LoadTmpls()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var addVariation = func(index int, callback func(mitem MenuItem) bool) {
|
|
renderBuffer, variableIndices, pathList := hold.Scan(tmpls, callback)
|
|
hold.Variations[index] = menuTmpl{renderBuffer, variableIndices, pathList}
|
|
}
|
|
|
|
// Guest Menu
|
|
addVariation(0, func(mitem MenuItem) bool {
|
|
return !mitem.MemberOnly
|
|
})
|
|
// Member Menu
|
|
addVariation(1, func(mitem MenuItem) bool {
|
|
return !mitem.SuperModOnly && !mitem.GuestOnly
|
|
})
|
|
// Super Mod Menu
|
|
addVariation(2, func(mitem MenuItem) bool {
|
|
return !mitem.AdminOnly && !mitem.GuestOnly
|
|
})
|
|
// Admin Menu
|
|
addVariation(3, func(mitem MenuItem) bool {
|
|
return !mitem.GuestOnly
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func nextCharIs(tmplData []byte, i int, expects byte) bool {
|
|
if len(tmplData) <= (i + 1) {
|
|
return false
|
|
}
|
|
return tmplData[i+1] == expects
|
|
}
|
|
|
|
func peekNextChar(tmplData []byte, i int) byte {
|
|
if len(tmplData) <= (i + 1) {
|
|
return 0
|
|
}
|
|
return tmplData[i+1]
|
|
}
|
|
|
|
func skipUntilIfExists(tmplData []byte, i int, expects byte) (newI int, hasIt bool) {
|
|
j := i
|
|
for ; j < len(tmplData); j++ {
|
|
if tmplData[j] == expects {
|
|
return j, true
|
|
}
|
|
}
|
|
return j, false
|
|
}
|
|
|
|
func skipUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) {
|
|
j := i
|
|
expectIndex := 0
|
|
for ; j < len(tmplData) && expectIndex < len(expects); j++ {
|
|
//fmt.Println("tmplData[j]: ", string(tmplData[j]))
|
|
if tmplData[j] != expects[expectIndex] {
|
|
return j, false
|
|
}
|
|
//fmt.Printf("found %+v at %d\n", string(expects[expectIndex]), expectIndex)
|
|
expectIndex++
|
|
}
|
|
return j, true
|
|
}
|
|
|
|
func skipAllUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) {
|
|
j := i
|
|
expectIndex := 0
|
|
for ; j < len(tmplData) && expectIndex < len(expects); j++ {
|
|
if tmplData[j] == expects[expectIndex] {
|
|
//fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex)
|
|
expectIndex++
|
|
if len(expects) <= expectIndex {
|
|
break
|
|
}
|
|
} else {
|
|
/*if expectIndex != 0 {
|
|
fmt.Println("broke expectations")
|
|
fmt.Println("expected: ", string(expects[expectIndex]))
|
|
fmt.Println("got: ", string(tmplData[j]))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j)))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j+1)))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j+2)))
|
|
fmt.Println("next: ", string(peekNextChar(tmplData, j+3)))
|
|
}*/
|
|
expectIndex = 0
|
|
}
|
|
}
|
|
return j, len(expects) == expectIndex
|
|
}
|
|
|
|
type menuRenderItem struct {
|
|
Type int // 0: text, 1: variable
|
|
Index int
|
|
}
|
|
|
|
type MenuTmpl struct {
|
|
Name string
|
|
TextBuffer [][]byte
|
|
VariableBuffer [][]byte
|
|
RenderList []menuRenderItem
|
|
}
|
|
|
|
func menuDumpSlice(outerSlice [][]byte) {
|
|
for sliceID, slice := range outerSlice {
|
|
fmt.Print(strconv.Itoa(sliceID) + ":[")
|
|
for _, char := range slice {
|
|
fmt.Print(string(char))
|
|
}
|
|
fmt.Print("] ")
|
|
}
|
|
}
|
|
|
|
func (hold *MenuListHolder) Parse(name string, tmplData []byte) (menuTmpl MenuTmpl) {
|
|
var textBuffer, variableBuffer [][]byte
|
|
var renderList []menuRenderItem
|
|
var subBuffer []byte
|
|
|
|
// ? We only support simple properties on MenuItem right now
|
|
var addVariable = func(name []byte) {
|
|
// TODO: Check if the subBuffer has any items or is empty
|
|
textBuffer = append(textBuffer, subBuffer)
|
|
subBuffer = nil
|
|
|
|
variableBuffer = append(variableBuffer, name)
|
|
renderList = append(renderList, menuRenderItem{0, len(textBuffer) - 1})
|
|
renderList = append(renderList, menuRenderItem{1, len(variableBuffer) - 1})
|
|
}
|
|
|
|
tmplData = bytes.Replace(tmplData, []byte("{{"), []byte("{"), -1)
|
|
tmplData = bytes.Replace(tmplData, []byte("}}"), []byte("}}"), -1)
|
|
for i := 0; i < len(tmplData); i++ {
|
|
char := tmplData[i]
|
|
if char == '{' {
|
|
dotIndex, hasDot := skipUntilIfExists(tmplData, i, '.')
|
|
if !hasDot {
|
|
// Template function style
|
|
langIndex, hasChars := skipUntilCharsExist(tmplData, i+1, []byte("lang"))
|
|
if hasChars {
|
|
startIndex, hasStart := skipUntilIfExists(tmplData, langIndex, '"')
|
|
endIndex, hasEnd := skipUntilIfExists(tmplData, startIndex+1, '"')
|
|
if hasStart && hasEnd {
|
|
fenceIndex, hasFence := skipUntilIfExists(tmplData, endIndex, '}')
|
|
if !hasFence || !nextCharIs(tmplData, fenceIndex, '}') {
|
|
break
|
|
}
|
|
//fmt.Println("tmplData[startIndex:endIndex]: ", tmplData[startIndex+1:endIndex])
|
|
prefix := []byte("lang.")
|
|
addVariable(append(prefix, tmplData[startIndex+1:endIndex]...))
|
|
i = fenceIndex + 1
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
fenceIndex, hasFence := skipUntilIfExists(tmplData, dotIndex, '}')
|
|
if !hasFence {
|
|
break
|
|
}
|
|
addVariable(tmplData[dotIndex:fenceIndex])
|
|
i = fenceIndex + 1
|
|
continue
|
|
}
|
|
subBuffer = append(subBuffer, char)
|
|
}
|
|
if len(subBuffer) > 0 {
|
|
// TODO: Have a property in renderList which holds the byte slice since variableBuffers and textBuffers have the same underlying implementation?
|
|
textBuffer = append(textBuffer, subBuffer)
|
|
renderList = append(renderList, menuRenderItem{0, len(textBuffer) - 1})
|
|
}
|
|
|
|
return MenuTmpl{name, textBuffer, variableBuffer, renderList}
|
|
}
|
|
|
|
func (hold *MenuListHolder) Scan(menuTmpls map[string]MenuTmpl, showItem func(mitem MenuItem) bool) (renderBuffer [][]byte, variableIndices []int, pathList []menuPath) {
|
|
for _, mitem := range hold.List {
|
|
// Do we want this item in this variation of the menu?
|
|
if !showItem(mitem) {
|
|
continue
|
|
}
|
|
renderBuffer, variableIndices = hold.ScanItem(menuTmpls, mitem, renderBuffer, variableIndices)
|
|
pathList = append(pathList, menuPath{mitem.Path, len(renderBuffer) - 1})
|
|
}
|
|
|
|
// TODO: Need more coalescing in the renderBuffer
|
|
return renderBuffer, variableIndices, pathList
|
|
}
|
|
|
|
// Note: This doesn't do a visibility check like hold.Scan() does
|
|
func (hold *MenuListHolder) ScanItem(menuTmpls map[string]MenuTmpl, mitem MenuItem, renderBuffer [][]byte, variableIndices []int) ([][]byte, []int) {
|
|
menuTmpl, ok := menuTmpls[mitem.TmplName]
|
|
if !ok {
|
|
menuTmpl = menuTmpls["menu_item"]
|
|
}
|
|
|
|
for _, renderItem := range menuTmpl.RenderList {
|
|
if renderItem.Type == 0 {
|
|
renderBuffer = append(renderBuffer, menuTmpl.TextBuffer[renderItem.Index])
|
|
continue
|
|
}
|
|
|
|
variable := menuTmpl.VariableBuffer[renderItem.Index]
|
|
dotAt, hasDot := skipUntilIfExists(variable, 0, '.')
|
|
if !hasDot {
|
|
continue
|
|
}
|
|
|
|
if bytes.Equal(variable[:dotAt], []byte("lang")) {
|
|
renderBuffer = append(renderBuffer, []byte(phrases.GetTmplPhrase(string(bytes.TrimPrefix(variable[dotAt:], []byte("."))))))
|
|
continue
|
|
}
|
|
|
|
var renderItem []byte
|
|
switch string(variable) {
|
|
case ".ID":
|
|
renderItem = []byte(strconv.Itoa(mitem.ID))
|
|
case ".Name":
|
|
renderItem = []byte(mitem.Name)
|
|
case ".HTMLID":
|
|
renderItem = []byte(mitem.HTMLID)
|
|
case ".CSSClass":
|
|
renderItem = []byte(mitem.CSSClass)
|
|
case ".Position":
|
|
renderItem = []byte(mitem.Position)
|
|
case ".Path":
|
|
renderItem = []byte(mitem.Path)
|
|
case ".Aria":
|
|
renderItem = []byte(mitem.Aria)
|
|
case ".Tooltip":
|
|
renderItem = []byte(mitem.Tooltip)
|
|
case ".CSSActive":
|
|
renderItem = []byte("{dyn.active}")
|
|
}
|
|
|
|
_, hasInnerVar := skipUntilIfExists(renderItem, 0, '{')
|
|
if hasInnerVar {
|
|
DebugLog("inner var: ", string(renderItem))
|
|
dotAt, hasDot := skipUntilIfExists(renderItem, 0, '.')
|
|
endFence, hasEndFence := skipUntilIfExists(renderItem, dotAt, '}')
|
|
if !hasDot || !hasEndFence || (endFence-dotAt) <= 1 {
|
|
renderBuffer = append(renderBuffer, renderItem)
|
|
variableIndices = append(variableIndices, len(renderBuffer)-1)
|
|
continue
|
|
}
|
|
|
|
if bytes.Equal(renderItem[1:dotAt], []byte("lang")) {
|
|
//fmt.Println("lang var: ", string(renderItem[dotAt+1:endFence]))
|
|
renderBuffer = append(renderBuffer, []byte(phrases.GetTmplPhrase(string(renderItem[dotAt+1:endFence]))))
|
|
} else {
|
|
fmt.Println("other var: ", string(variable[:dotAt]))
|
|
if len(renderItem) > 0 {
|
|
renderBuffer = append(renderBuffer, renderItem)
|
|
variableIndices = append(variableIndices, len(renderBuffer)-1)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if len(renderItem) > 0 {
|
|
renderBuffer = append(renderBuffer, renderItem)
|
|
}
|
|
}
|
|
return renderBuffer, variableIndices
|
|
}
|
|
|
|
// TODO: Pre-render the lang stuff
|
|
func (hold *MenuListHolder) Build(w io.Writer, user *User, pathPrefix string) error {
|
|
var mTmpl menuTmpl
|
|
if !user.Loggedin {
|
|
mTmpl = hold.Variations[0]
|
|
} else if user.IsAdmin {
|
|
mTmpl = hold.Variations[3]
|
|
} else if user.IsSuperMod {
|
|
mTmpl = hold.Variations[2]
|
|
} else {
|
|
mTmpl = hold.Variations[1]
|
|
}
|
|
if pathPrefix == "" {
|
|
pathPrefix = Config.DefaultPath
|
|
}
|
|
|
|
if len(mTmpl.VariableIndices) == 0 {
|
|
for _, renderItem := range mTmpl.RenderBuffer {
|
|
w.Write(renderItem)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var nearIndex = 0
|
|
for index, renderItem := range mTmpl.RenderBuffer {
|
|
if index != mTmpl.VariableIndices[nearIndex] {
|
|
w.Write(renderItem)
|
|
continue
|
|
}
|
|
variable := renderItem
|
|
// ? - I can probably remove this check now that I've kicked it upstream, or we could keep it here for safety's sake?
|
|
if len(variable) == 0 {
|
|
continue
|
|
}
|
|
|
|
prevIndex := 0
|
|
for i := 0; i < len(renderItem); i++ {
|
|
fenceStart, hasFence := skipUntilIfExists(variable, i, '{')
|
|
if !hasFence {
|
|
continue
|
|
}
|
|
i = fenceStart
|
|
fenceEnd, hasFence := skipUntilIfExists(variable, fenceStart, '}')
|
|
if !hasFence {
|
|
continue
|
|
}
|
|
i = fenceEnd
|
|
dotAt, hasDot := skipUntilIfExists(variable, fenceStart, '.')
|
|
if !hasDot {
|
|
continue
|
|
}
|
|
|
|
switch string(variable[fenceStart+1 : dotAt]) {
|
|
case "me":
|
|
w.Write(variable[prevIndex:fenceStart])
|
|
switch string(variable[dotAt+1 : fenceEnd]) {
|
|
case "Link":
|
|
w.Write([]byte(user.Link))
|
|
case "Session":
|
|
w.Write([]byte(user.Session))
|
|
}
|
|
prevIndex = fenceEnd
|
|
// TODO: Optimise this
|
|
case "dyn":
|
|
w.Write(variable[prevIndex:fenceStart])
|
|
var pmi int
|
|
for ii, pathItem := range mTmpl.PathMappings {
|
|
pmi = ii
|
|
if pathItem.Index > index {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(mTmpl.PathMappings) != 0 {
|
|
path := mTmpl.PathMappings[pmi].Path
|
|
if path == "" || path == "/" {
|
|
path = Config.DefaultPath
|
|
}
|
|
if strings.HasPrefix(path, pathPrefix) {
|
|
w.Write([]byte(" menu_active"))
|
|
}
|
|
}
|
|
|
|
prevIndex = fenceEnd
|
|
}
|
|
}
|
|
|
|
w.Write(variable[prevIndex : len(variable)-1])
|
|
if len(mTmpl.VariableIndices) > (nearIndex + 1) {
|
|
nearIndex++
|
|
}
|
|
}
|
|
return nil
|
|
}
|