7be011a30d
The topic list cache can handle more groups now, but don't go too crazy with groups (e.g. thousands of them). Make the suspicious request logs more descriptive. Added the phrases API endpoint. Split the template phrases up by prefix, more work on this coming up. Removed #dash_saved and part of #dash_username. Removed some temporary artifacts from trying to implement FA5 in Nox. Removed some commented CSS. Fixed template artifact deletion on Windows. Tweaked HTTPSRedirect to make it more compact. Fixed NullUserCache not complying with the expectations for BulkGet. Swapped out a few RunVhook calls for more appropriate RunVhookNoreturn calls. Removed a few redundant IsAdmin checks when IsMod would suffice. Commented out a few pushers. Desktop notification permission requests are no longer served to guests. Split topics.html into topics.html and topics_topic.html RunThemeTemplate should now fallback to interpreted templates properly when the transpiled variants aren't avaialb.e Changed TopicsRow.CreatedAt from a string to a time.Time Added SkipTmplPtrMap to CTemplateConfig. Added SetBuildTags to CTemplateSet. A bit more data is dumped when something goes wrong while transpiling templates now. topics_topic, topic_posts, and topic_alt_posts are now transpiled for the client, although not all of them are ready to be served to the client yet. Client rendered templates now support phrases. Client rendered templates now support loops. Fixed loadAlerts in global.js Refactored some of the template initialisation code to make it less repetitive. Split topic.html into topic.html and topic_posts.html Split topic_alt.html into topic_alt.html and topic_alt_posts.html Added comments for PollCache. Fixed a data race in the MemoryPollCache. The writer is now closed properly in WsHubImpl.broadcastMessage. Fixed a potential deadlock in WsHubImpl.broadcastMessage. Removed some old commented code in websockets.go Added the DisableLiveTopicList config setting.
494 lines
14 KiB
Go
494 lines
14 KiB
Go
package common
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"strconv"
|
|
|
|
"../query_gen/lib"
|
|
)
|
|
|
|
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 menuTmpl struct {
|
|
RenderBuffer [][]byte
|
|
VariableIndices []int
|
|
}
|
|
|
|
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 := hold.Scan(tmpls, callback)
|
|
hold.Variations[index] = menuTmpl{renderBuffer, variableIndices}
|
|
//fmt.Print("renderBuffer: ")
|
|
//menuDumpSlice(renderBuffer)
|
|
//fmt.Printf("\nvariableIndices: %+v\n", variableIndices)
|
|
}
|
|
|
|
// 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
|
|
//fmt.Printf("tmplData: %+v\n", string(tmplData))
|
|
for ; j < len(tmplData) && expectIndex < len(expects); j++ {
|
|
//fmt.Println("j: ", j)
|
|
//fmt.Println("tmplData[j]: ", string(tmplData[j])+" ")
|
|
if tmplData[j] == expects[expectIndex] {
|
|
//fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex)
|
|
expectIndex++
|
|
if len(expects) <= expectIndex {
|
|
//fmt.Println("breaking")
|
|
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
|
|
}
|
|
}
|
|
//fmt.Println("len(expects): ", len(expects))
|
|
//fmt.Println("expectIndex: ", expectIndex)
|
|
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) {
|
|
//fmt.Println("tmplData: ", string(tmplData))
|
|
var textBuffer, variableBuffer [][]byte
|
|
var renderList []menuRenderItem
|
|
var subBuffer []byte
|
|
|
|
// ? We only support simple properties on MenuItem right now
|
|
var addVariable = func(name []byte) {
|
|
//fmt.Println("appending subBuffer: ", string(subBuffer))
|
|
// TODO: Check if the subBuffer has any items or is empty
|
|
textBuffer = append(textBuffer, subBuffer)
|
|
subBuffer = nil
|
|
|
|
//fmt.Println("adding variable: ", string(name))
|
|
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 == '{' {
|
|
//fmt.Println("found open fence")
|
|
dotIndex, hasDot := skipUntilIfExists(tmplData, i, '.')
|
|
if !hasDot {
|
|
//fmt.Println("no dot, assumed template function style")
|
|
// 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 {
|
|
//fmt.Println("no end fence")
|
|
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})
|
|
}
|
|
|
|
//fmt.Println("name: ", name)
|
|
//fmt.Print("textBuffer: ")
|
|
//menuDumpSlice(textBuffer)
|
|
//fmt.Print("\nvariableBuffer: ")
|
|
//menuDumpSlice(variableBuffer)
|
|
//fmt.Printf("\nrenderList: %+v\n", renderList)
|
|
return MenuTmpl{name, textBuffer, variableBuffer, renderList}
|
|
}
|
|
|
|
func (hold *MenuListHolder) Scan(menuTmpls map[string]MenuTmpl, showItem func(mitem MenuItem) bool) (renderBuffer [][]byte, variableIndices []int) {
|
|
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)
|
|
}
|
|
// TODO: Need more coalescing in the renderBuffer
|
|
return renderBuffer, variableIndices
|
|
}
|
|
|
|
// 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"]
|
|
}
|
|
|
|
//fmt.Println("menuTmpl: ", menuTmpl)
|
|
for _, renderItem := range menuTmpl.RenderList {
|
|
if renderItem.Type == 0 {
|
|
renderBuffer = append(renderBuffer, menuTmpl.TextBuffer[renderItem.Index])
|
|
continue
|
|
}
|
|
|
|
variable := menuTmpl.VariableBuffer[renderItem.Index]
|
|
//fmt.Println("initial variable: ", string(variable))
|
|
dotAt, hasDot := skipUntilIfExists(variable, 0, '.')
|
|
if !hasDot {
|
|
//fmt.Println("no dot")
|
|
continue
|
|
}
|
|
|
|
if bytes.Equal(variable[:dotAt], []byte("lang")) {
|
|
//fmt.Println("lang: ", string(bytes.TrimPrefix(variable[dotAt:], []byte("."))))
|
|
renderBuffer = append(renderBuffer, []byte(GetTmplPhrase(string(bytes.TrimPrefix(variable[dotAt:], []byte("."))))))
|
|
} else {
|
|
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)
|
|
}
|
|
|
|
_, hasInnerVar := skipUntilIfExists(renderItem, 0, '{')
|
|
if hasInnerVar {
|
|
//fmt.Println("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(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
|
|
}
|
|
|
|
//fmt.Println("normal var: ", string(variable[:dotAt]))
|
|
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) 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 len(mTmpl.VariableIndices) == 0 {
|
|
//fmt.Println("no variable indices")
|
|
for _, renderItem := range mTmpl.RenderBuffer {
|
|
//fmt.Printf("renderItem: %+v\n", renderItem)
|
|
w.Write(renderItem)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var nearIndex = 0
|
|
for index, renderItem := range mTmpl.RenderBuffer {
|
|
if index != mTmpl.VariableIndices[nearIndex] {
|
|
//fmt.Println("wrote text: ", string(renderItem))
|
|
w.Write(renderItem)
|
|
continue
|
|
}
|
|
|
|
//fmt.Println("variable: ", string(renderItem))
|
|
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
|
|
}
|
|
//fmt.Println("checking me: ", string(variable[fenceStart+1:dotAt]))
|
|
if bytes.Equal(variable[fenceStart+1:dotAt], []byte("me")) {
|
|
//fmt.Println("maybe me variable")
|
|
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
|
|
}
|
|
}
|
|
//fmt.Println("prevIndex: ", prevIndex)
|
|
//fmt.Println("len(variable)-1: ", len(variable)-1)
|
|
w.Write(variable[prevIndex : len(variable)-1])
|
|
if len(mTmpl.VariableIndices) > (nearIndex + 1) {
|
|
nearIndex++
|
|
}
|
|
}
|
|
return nil
|
|
}
|