Refactored the plugin system to use a hook table. This should help to reduce the probability of data races and strange bugs, as-well as helping to abstract certain details, so future changes are less likely to break things.

Fixed the missing background for the create topic non-JS page in Nox.
The non-JS create topic page now has a localised title.
This commit is contained in:
Azareal 2018-10-21 23:54:32 +10:00
parent 218070fceb
commit 876ae4e245
15 changed files with 201 additions and 174 deletions

View File

@ -29,9 +29,9 @@ func SendValidationEmail(username string, email string, token string) bool {
// TODO: Add support for TLS // TODO: Add support for TLS
func SendEmail(email string, subject string, msg string) bool { func SendEmail(email string, subject string, msg string) bool {
// This hook is useful for plugin_sendmail or for testing tools. Possibly to hook it into some sort of mail server? // This hook is useful for plugin_sendmail or for testing tools. Possibly to hook it into some sort of mail server?
// TODO: Abstract this ret, hasHook := GetHookTable().VhookNeedHook("email_send_intercept", email, subject, msg)
if Vhooks["email_send_intercept"] != nil { if hasHook {
return Vhooks["email_send_intercept"](email, subject, msg).(bool) return ret.(bool)
} }
body := "Subject: " + subject + "\n\n" + msg + "\n" body := "Subject: " + subject + "\n\n" + msg + "\n"

View File

@ -12,6 +12,7 @@ import (
"errors" "errors"
"log" "log"
"net/http" "net/http"
"sync"
"sync/atomic" "sync/atomic"
"../query_gen/lib" "../query_gen/lib"
@ -48,40 +49,133 @@ var hookTableBox atomic.Value
// TODO: Make the RunXHook functions methods on HookTable // TODO: Make the RunXHook functions methods on HookTable
// TODO: Have plugins update hooks on a mutex guarded map and create a copy of that map in a serial global goroutine which gets thrown in the atomic.Value // TODO: Have plugins update hooks on a mutex guarded map and create a copy of that map in a serial global goroutine which gets thrown in the atomic.Value
type HookTable struct { type HookTable struct {
Hooks map[string][]func(interface{}) interface{} Hooks map[string][]func(interface{}) interface{}
Vhooks map[string]func(...interface{}) interface{} Vhooks map[string]func(...interface{}) interface{}
VhookSkippable map[string]func(...interface{}) (bool, RouteError) VhookSkippable_ map[string]func(...interface{}) (bool, RouteError)
Sshooks map[string][]func(string) string Sshooks map[string][]func(string) string
PreRenderHooks map[string][]func(http.ResponseWriter, *http.Request, *User, interface{}) bool PreRenderHooks map[string][]func(http.ResponseWriter, *http.Request, *User, interface{}) bool
// For future use: // For future use:
messageHooks map[string][]func(Message, PageInt, ...interface{}) interface{} messageHooks map[string][]func(Message, PageInt, ...interface{}) interface{}
} }
func init() { func init() {
hookTableBox.Store(new(HookTable)) RebuildHookTable()
}
// For extend.go use only, access this via GetHookTable() elsewhere
var hookTable = &HookTable{
map[string][]func(interface{}) interface{}{
"forums_frow_assign": nil,
"topic_create_frow_assign": nil,
},
map[string]func(...interface{}) interface{}{
"forum_trow_assign": nil,
"topics_topic_row_assign": nil,
//"topics_user_row_assign": nil,
"topic_reply_row_assign": nil,
"create_group_preappend": nil, // What is this? Investigate!
"topic_create_pre_loop": nil,
},
map[string]func(...interface{}) (bool, RouteError){
"simple_forum_check_pre_perms": nil,
"forum_check_pre_perms": nil,
},
map[string][]func(string) string{
"preparse_preassign": nil,
"parse_assign": nil,
},
nil,
nil,
}
var hookTableUpdateMutex sync.Mutex
func RebuildHookTable() {
hookTableUpdateMutex.Lock()
defer hookTableUpdateMutex.Unlock()
unsafeRebuildHookTable()
}
func unsafeRebuildHookTable() {
ihookTable := new(HookTable)
*ihookTable = *hookTable
hookTableBox.Store(ihookTable)
}
func GetHookTable() *HookTable {
return hookTableBox.Load().(*HookTable)
} }
// Hooks with a single argument. Is this redundant? Might be useful for inlining, as variadics aren't inlined? Are closures even inlined to begin with? // Hooks with a single argument. Is this redundant? Might be useful for inlining, as variadics aren't inlined? Are closures even inlined to begin with?
var Hooks = map[string][]func(interface{}) interface{}{ func (table *HookTable) Hook(name string, data interface{}) interface{} {
"forums_frow_assign": nil, hooks, ok := table.Hooks[name]
"topic_create_frow_assign": nil, if ok {
for _, hook := range hooks {
data = hook(data)
}
}
return data
}
// To cover the case in routes/topic.go's CreateTopic route, we could probably obsolete this use and replace it
func (table *HookTable) HookSkippable(name string, data interface{}) (skip bool) {
hooks, ok := table.Hooks[name]
if ok {
for _, hook := range hooks {
skip = hook(data).(bool)
if skip {
break
}
}
}
return skip
} }
// Hooks with a variable number of arguments // Hooks with a variable number of arguments
var Vhooks = map[string]func(...interface{}) interface{}{ // TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn?
"forum_trow_assign": nil, func (table *HookTable) Vhook(name string, data ...interface{}) interface{} {
"topics_topic_row_assign": nil, hook := table.Vhooks[name]
//"topics_user_row_assign": nil, if hook != nil {
"topic_reply_row_assign": nil, return hook(data...)
"create_group_preappend": nil, // What is this? Investigate! }
"topic_create_pre_loop": nil, return nil
}
func (table *HookTable) VhookNoRet(name string, data ...interface{}) {
hook := table.Vhooks[name]
if hook != nil {
_ = hook(data...)
}
}
// TODO: Find a better way of doing this
func (table *HookTable) VhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) {
hook := table.Vhooks[name]
if hook != nil {
return hook(data...), true
}
return nil, false
} }
// Hooks with a variable number of arguments and return values for skipping the parent function and propagating an error upwards // Hooks with a variable number of arguments and return values for skipping the parent function and propagating an error upwards
var VhookSkippable = map[string]func(...interface{}) (bool, RouteError){ func (table *HookTable) VhookSkippable(name string, data ...interface{}) (bool, RouteError) {
"simple_forum_check_pre_perms": nil, hook := table.VhookSkippable_[name]
"forum_check_pre_perms": nil, if hook != nil {
return hook(data...)
}
return false, nil
}
// Hooks which take in and spit out a string. This is usually used for parser components
// Trying to get a teeny bit of type-safety where-ever possible, especially for such a critical set of hooks
func (table *HookTable) Sshook(name string, data string) string {
ssHooks, ok := table.Sshooks[name]
if ok {
for _, hook := range ssHooks {
data = hook(data)
}
}
return data
} }
//var vhookErrorable = map[string]func(...interface{}) (interface{}, RouteError){} //var vhookErrorable = map[string]func(...interface{}) (interface{}, RouteError){}
@ -117,12 +211,6 @@ var messageHooks = map[string][]func(Message, PageInt, ...interface{}) interface
"topic_reply_row_assign": nil, "topic_reply_row_assign": nil,
} }
// Hooks which take in and spit out a string. This is usually used for parser components
var Sshooks = map[string][]func(string) string{
"preparse_preassign": nil,
"parse_assign": nil,
}
// The hooks which run before the template is rendered for a route // The hooks which run before the template is rendered for a route
var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User, interface{}) bool{ var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User, interface{}) bool{
"pre_render": nil, "pre_render": nil,
@ -317,87 +405,76 @@ func (plugins PluginList) Load() error {
// ? - Is this racey? // ? - Is this racey?
// TODO: Generate the cases in this switch // TODO: Generate the cases in this switch
func (plugin *Plugin) AddHook(name string, handler interface{}) { func (plugin *Plugin) AddHook(name string, handler interface{}) {
hookTableUpdateMutex.Lock()
defer hookTableUpdateMutex.Unlock()
switch h := handler.(type) { switch h := handler.(type) {
case func(interface{}) interface{}: case func(interface{}) interface{}:
if len(Hooks[name]) == 0 { if len(hookTable.Hooks[name]) == 0 {
var hookSlice []func(interface{}) interface{} hookTable.Hooks[name] = []func(interface{}) interface{}{}
hookSlice = append(hookSlice, h)
Hooks[name] = hookSlice
} else {
Hooks[name] = append(Hooks[name], h)
} }
plugin.Hooks[name] = len(Hooks[name]) - 1 hookTable.Hooks[name] = append(hookTable.Hooks[name], h)
plugin.Hooks[name] = len(hookTable.Hooks[name]) - 1
case func(string) string: case func(string) string:
if len(Sshooks[name]) == 0 { if len(hookTable.Sshooks[name]) == 0 {
var hookSlice []func(string) string hookTable.Sshooks[name] = []func(string) string{}
hookSlice = append(hookSlice, h)
Sshooks[name] = hookSlice
} else {
Sshooks[name] = append(Sshooks[name], h)
} }
plugin.Hooks[name] = len(Sshooks[name]) - 1 hookTable.Sshooks[name] = append(hookTable.Sshooks[name], h)
plugin.Hooks[name] = len(hookTable.Sshooks[name]) - 1
case func(http.ResponseWriter, *http.Request, *User, interface{}) bool: case func(http.ResponseWriter, *http.Request, *User, interface{}) bool:
if len(PreRenderHooks[name]) == 0 { if len(PreRenderHooks[name]) == 0 {
var hookSlice []func(http.ResponseWriter, *http.Request, *User, interface{}) bool PreRenderHooks[name] = []func(http.ResponseWriter, *http.Request, *User, interface{}) bool{}
hookSlice = append(hookSlice, h)
PreRenderHooks[name] = hookSlice
} else {
PreRenderHooks[name] = append(PreRenderHooks[name], h)
} }
PreRenderHooks[name] = append(PreRenderHooks[name], h)
plugin.Hooks[name] = len(PreRenderHooks[name]) - 1 plugin.Hooks[name] = len(PreRenderHooks[name]) - 1
case func() error: // ! We might want a more generic name, as we might use this signature for things other than tasks hooks case func() error: // ! We might want a more generic name, as we might use this signature for things other than tasks hooks
if len(taskHooks[name]) == 0 { if len(taskHooks[name]) == 0 {
var hookSlice []func() error taskHooks[name] = []func() error{}
hookSlice = append(hookSlice, h)
taskHooks[name] = hookSlice
} else {
taskHooks[name] = append(taskHooks[name], h)
} }
taskHooks[name] = append(taskHooks[name], h)
plugin.Hooks[name] = len(taskHooks[name]) - 1 plugin.Hooks[name] = len(taskHooks[name]) - 1
case func(...interface{}) interface{}: case func(...interface{}) interface{}:
Vhooks[name] = h hookTable.Vhooks[name] = h
plugin.Hooks[name] = 0 plugin.Hooks[name] = 0
case func(...interface{}) (bool, RouteError): case func(...interface{}) (bool, RouteError):
VhookSkippable[name] = h hookTable.VhookSkippable_[name] = h
plugin.Hooks[name] = 0 plugin.Hooks[name] = 0
default: default:
panic("I don't recognise this kind of handler!") // Should this be an error for the plugin instead of a panic()? panic("I don't recognise this kind of handler!") // Should this be an error for the plugin instead of a panic()?
} }
// TODO: Do this once during plugin activation / deactivation rather than doing it for each hook
unsafeRebuildHookTable()
} }
// ? - Is this racey? // ? - Is this racey?
// TODO: Generate the cases in this switch // TODO: Generate the cases in this switch
func (plugin *Plugin) RemoveHook(name string, handler interface{}) { func (plugin *Plugin) RemoveHook(name string, handler interface{}) {
hookTableUpdateMutex.Lock()
defer hookTableUpdateMutex.Unlock()
key, ok := plugin.Hooks[name]
if !ok {
panic("handler not registered as hook")
}
switch handler.(type) { switch handler.(type) {
case func(interface{}) interface{}: case func(interface{}) interface{}:
key, ok := plugin.Hooks[name] hook := hookTable.Hooks[name]
if !ok {
panic("handler not registered as hook")
}
hook := Hooks[name]
if len(hook) == 1 { if len(hook) == 1 {
hook = []func(interface{}) interface{}{} hook = []func(interface{}) interface{}{}
} else { } else {
hook = append(hook[:key], hook[key+1:]...) hook = append(hook[:key], hook[key+1:]...)
} }
Hooks[name] = hook hookTable.Hooks[name] = hook
case func(string) string: case func(string) string:
key, ok := plugin.Hooks[name] hook := hookTable.Sshooks[name]
if !ok {
panic("handler not registered as hook")
}
hook := Sshooks[name]
if len(hook) == 1 { if len(hook) == 1 {
hook = []func(string) string{} hook = []func(string) string{}
} else { } else {
hook = append(hook[:key], hook[key+1:]...) hook = append(hook[:key], hook[key+1:]...)
} }
Sshooks[name] = hook hookTable.Sshooks[name] = hook
case func(http.ResponseWriter, *http.Request, *User, interface{}) bool: case func(http.ResponseWriter, *http.Request, *User, interface{}) bool:
key, ok := plugin.Hooks[name]
if !ok {
panic("handler not registered as hook")
}
hook := PreRenderHooks[name] hook := PreRenderHooks[name]
if len(hook) == 1 { if len(hook) == 1 {
hook = []func(http.ResponseWriter, *http.Request, *User, interface{}) bool{} hook = []func(http.ResponseWriter, *http.Request, *User, interface{}) bool{}
@ -406,10 +483,6 @@ func (plugin *Plugin) RemoveHook(name string, handler interface{}) {
} }
PreRenderHooks[name] = hook PreRenderHooks[name] = hook
case func() error: case func() error:
key, ok := plugin.Hooks[name]
if !ok {
panic("handler not registered as hook")
}
hook := taskHooks[name] hook := taskHooks[name]
if len(hook) == 1 { if len(hook) == 1 {
hook = []func() error{} hook = []func() error{}
@ -418,13 +491,15 @@ func (plugin *Plugin) RemoveHook(name string, handler interface{}) {
} }
taskHooks[name] = hook taskHooks[name] = hook
case func(...interface{}) interface{}: case func(...interface{}) interface{}:
delete(Vhooks, name) delete(hookTable.Vhooks, name)
case func(...interface{}) (bool, RouteError): case func(...interface{}) (bool, RouteError):
delete(VhookSkippable, name) delete(hookTable.VhookSkippable_, name)
default: default:
panic("I don't recognise this kind of handler!") // Should this be an error for the plugin instead of a panic()? panic("I don't recognise this kind of handler!") // Should this be an error for the plugin instead of a panic()?
} }
delete(plugin.Hooks, name) delete(plugin.Hooks, name)
// TODO: Do this once during plugin activation / deactivation rather than doing it for each hook
unsafeRebuildHookTable()
} }
// TODO: Add a HasHook method to complete the AddHook, RemoveHook, etc. set? // TODO: Add a HasHook method to complete the AddHook, RemoveHook, etc. set?
@ -450,55 +525,6 @@ func InitPlugins() {
} }
// ? - Are the following functions racey? // ? - Are the following functions racey?
func RunHook(name string, data interface{}) interface{} {
hooks, ok := Hooks[name]
if ok {
for _, hook := range hooks {
data = hook(data)
}
}
return data
}
func RunHookNoreturn(name string, data interface{}) {
hooks, ok := Hooks[name]
if !ok {
return
}
for _, hook := range hooks {
_ = hook(data)
}
}
// TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn?
func RunVhook(name string, data ...interface{}) interface{} {
hook := Vhooks[name]
if hook != nil {
return hook(data...)
}
return nil
}
func RunVhookSkippable(name string, data ...interface{}) (bool, RouteError) {
return VhookSkippable[name](data...)
}
func RunVhookNoreturn(name string, data ...interface{}) {
hook := Vhooks[name]
if hook != nil {
_ = hook(data...)
}
}
// TODO: Find a better way of doing this
func RunVhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) {
hook := Vhooks[name]
if hook != nil {
return hook(data...), true
}
return nil, false
}
func RunTaskHook(name string) error { func RunTaskHook(name string) error {
for _, hook := range taskHooks[name] { for _, hook := range taskHooks[name] {
err := hook() err := hook()
@ -509,17 +535,6 @@ func RunTaskHook(name string) error {
return nil return nil
} }
// Trying to get a teeny bit of type-safety where-ever possible, especially for such a critical set of hooks
func RunSshook(name string, data string) string {
ssHooks, ok := Sshooks[name]
if ok {
for _, hook := range ssHooks {
data = hook(data)
}
}
return data
}
func RunPreRenderHook(name string, w http.ResponseWriter, r *http.Request, user *User, data interface{}) (halt bool) { func RunPreRenderHook(name string, w http.ResponseWriter, r *http.Request, user *User, data interface{}) (halt bool) {
// This hook runs on ALL PreRender hooks // This hook runs on ALL PreRender hooks
preRenderHooks, ok := PreRenderHooks["pre_render"] preRenderHooks, ok := PreRenderHooks["pre_render"]

View File

@ -229,7 +229,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
var blankIntList []int var blankIntList []int
var pluginPerms = make(map[string]bool) var pluginPerms = make(map[string]bool)
var pluginPermsBytes = []byte("{}") var pluginPermsBytes = []byte("{}")
RunVhook("create_group_preappend", &pluginPerms, &pluginPermsBytes) GetHookTable().Vhook("create_group_preappend", &pluginPerms, &pluginPermsBytes)
// Generate the forum permissions based on the presets... // Generate the forum permissions based on the presets...
fdata, err := Forums.GetAll() fdata, err := Forums.GetAll()

View File

@ -23,6 +23,7 @@ type Header struct {
Theme *Theme Theme *Theme
//TemplateName string // TODO: Use this to move template calls to the router rather than duplicating them over and over and over? //TemplateName string // TODO: Use this to move template calls to the router rather than duplicating them over and over and over?
CurrentUser User // TODO: Deprecate CurrentUser on the page structs and use a pointer here CurrentUser User // TODO: Deprecate CurrentUser on the page structs and use a pointer here
Hooks *HookTable
Zone string Zone string
Path string Path string
MetaDesc string MetaDesc string
@ -50,6 +51,7 @@ func (header *Header) AddNotice(name string) {
type HeaderLite struct { type HeaderLite struct {
Site *site Site *site
Settings SettingMap Settings SettingMap
Hooks *HookTable
ExtData ExtData ExtData ExtData
} }

View File

@ -203,7 +203,7 @@ func PreparseMessage(msg string) string {
msg = strings.Replace(msg, " ", "", -1) msg = strings.Replace(msg, " ", "", -1)
msg = strings.Replace(msg, "\r", "", -1) // Windows artifact msg = strings.Replace(msg, "\r", "", -1) // Windows artifact
//msg = strings.Replace(msg, "\n\n\n\n", "\n\n\n", -1) //msg = strings.Replace(msg, "\n\n\n\n", "\n\n\n", -1)
msg = RunSshook("preparse_preassign", msg) msg = GetHookTable().Sshook("preparse_preassign", msg)
// There are a few useful cases for having spaces, but I'd like to stop the WYSIWYG from inserting random lines here and there // There are a few useful cases for having spaces, but I'd like to stop the WYSIWYG from inserting random lines here and there
msg = SanitiseBody(msg) msg = SanitiseBody(msg)
@ -624,7 +624,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
} }
msg = strings.Replace(msg, "\n", "<br>", -1) msg = strings.Replace(msg, "\n", "<br>", -1)
msg = RunSshook("parse_assign", msg) msg = GetHookTable().Sshook("parse_assign", msg)
return msg return msg
} }

View File

@ -20,28 +20,29 @@ var ForumUserCheck func(w http.ResponseWriter, r *http.Request, user *User, fid
var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, err RouteError) = simpleUserCheck var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, err RouteError) = simpleUserCheck
var UserCheck func(w http.ResponseWriter, r *http.Request, user *User) (header *Header, err RouteError) = userCheck var UserCheck func(w http.ResponseWriter, r *http.Request, user *User) (header *Header, err RouteError) = userCheck
func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerLite *HeaderLite, rerr RouteError) { func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) (header *HeaderLite, rerr RouteError) {
header, rerr = SimpleUserCheck(w, r, user)
if rerr != nil {
return header, rerr
}
if !Forums.Exists(fid) { if !Forums.Exists(fid) {
return nil, PreError("The target forum doesn't exist.", w, r) return nil, PreError("The target forum doesn't exist.", w, r)
} }
// Is there a better way of doing the skip AND the success flag on this hook like multiple returns? // Is there a better way of doing the skip AND the success flag on this hook like multiple returns?
if VhookSkippable["simple_forum_check_pre_perms"] != nil { skip, rerr := header.Hooks.VhookSkippable("simple_forum_check_pre_perms", w, r, user, &fid, &header)
var skip bool if skip || rerr != nil {
skip, rerr = RunVhookSkippable("simple_forum_check_pre_perms", w, r, user, &fid, &headerLite) return header, rerr
if skip || rerr != nil {
return headerLite, rerr
}
} }
fperms, err := FPStore.Get(fid, user.Group) fperms, err := FPStore.Get(fid, user.Group)
if err == ErrNoRows { if err == ErrNoRows {
fperms = BlankForumPerms() fperms = BlankForumPerms()
} else if err != nil { } else if err != nil {
return headerLite, InternalError(err, w, r) return header, InternalError(err, w, r)
} }
cascadeForumPerms(fperms, user) cascadeForumPerms(fperms, user)
return headerLite, nil return header, nil
} }
func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) (header *Header, rerr RouteError) { func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) (header *Header, rerr RouteError) {
@ -53,12 +54,9 @@ func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int)
return header, NotFound(w, r, header) return header, NotFound(w, r, header)
} }
if VhookSkippable["forum_check_pre_perms"] != nil { skip, rerr := header.Hooks.VhookSkippable("forum_check_pre_perms", w, r, user, &fid, &header)
var skip bool if skip || rerr != nil {
skip, rerr = RunVhookSkippable("forum_check_pre_perms", w, r, user, &fid, &header) return header, rerr
if skip || rerr != nil {
return header, rerr
}
} }
fperms, err := FPStore.Get(fid, user.Group) fperms, err := FPStore.Get(fid, user.Group)
@ -99,7 +97,6 @@ func cascadeForumPerms(fperms *ForumPerms, user *User) {
// TODO: Do a panel specific theme? // TODO: Do a panel specific theme?
func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Header, stats PanelStats, rerr RouteError) { func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Header, stats PanelStats, rerr RouteError) {
var theme = &Theme{Name: ""} var theme = &Theme{Name: ""}
cookie, err := r.Cookie("current_theme") cookie, err := r.Cookie("current_theme")
if err == nil { if err == nil {
inTheme, ok := Themes[html.EscapeString(cookie.Value)] inTheme, ok := Themes[html.EscapeString(cookie.Value)]
@ -117,6 +114,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
Themes: Themes, Themes: Themes,
Theme: theme, Theme: theme,
CurrentUser: *user, CurrentUser: *user,
Hooks: GetHookTable(),
Zone: "panel", Zone: "panel",
Writer: w, Writer: w,
} }
@ -155,7 +153,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
} }
func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) { func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {
return simpleUserCheck(w, r, user) return SimpleUserCheck(w, r, user)
} }
// SimpleUserCheck is back from the grave, yay :D // SimpleUserCheck is back from the grave, yay :D
@ -163,6 +161,7 @@ func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
return &HeaderLite{ return &HeaderLite{
Site: Site, Site: Site,
Settings: SettingBox.Load().(SettingMap), Settings: SettingBox.Load().(SettingMap),
Hooks: GetHookTable(),
}, nil }, nil
} }
@ -187,6 +186,7 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head
Themes: Themes, Themes: Themes,
Theme: theme, Theme: theme,
CurrentUser: *user, // ! Some things rely on this being a pointer downstream from this function CurrentUser: *user, // ! Some things rely on this being a pointer downstream from this function
Hooks: GetHookTable(),
Zone: "frontend", Zone: "frontend",
Writer: w, Writer: w,
} }

View File

@ -258,7 +258,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter
topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt) topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt)
// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/ // TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/
RunVhook("topics_topic_row_assign", &topicItem, &forum) GetHookTable().Vhook("topics_topic_row_assign", &topicItem, &forum)
topicList = append(topicList, &topicItem) topicList = append(topicList, &topicItem)
reqUserList[topicItem.CreatedBy] = true reqUserList[topicItem.CreatedBy] = true
reqUserList[topicItem.LastReplyBy] = true reqUserList[topicItem.LastReplyBy] = true

View File

@ -121,6 +121,7 @@
"page":"Page", "page":"Page",
"topics":"All Topics", "topics":"All Topics",
"forums":"Forum List", "forums":"Forum List",
"create_topic":"Create Topic",
"login":"Login", "login":"Login",
"login_mfa_verify":"2FA Verify", "login_mfa_verify":"2FA Verify",
"register":"Registration", "register":"Registration",

View File

@ -967,14 +967,23 @@ func TestPluginManager(t *testing.T) {
expectNilErr(t, plugin.SetActive(false)) expectNilErr(t, plugin.SetActive(false))
// Hook tests // Hook tests
expect(t, common.RunSshook("haha", "ho") == "ho", "Sshook shouldn't have anything bound to it yet") expect(t, common.GetHookTable().Sshook("haha", "ho") == "ho", "Sshook shouldn't have anything bound to it yet")
var handle = func(in string) (out string) { handle := func(in string) (out string) {
return in + "hi" return in + "hi"
} }
plugin.AddHook("haha", handle) plugin.AddHook("haha", handle)
expect(t, common.RunSshook("haha", "ho") == "hohi", "Sshook didn't give hohi") expect(t, common.GetHookTable().Sshook("haha", "ho") == "hohi", "Sshook didn't give hohi")
plugin.RemoveHook("haha", handle) plugin.RemoveHook("haha", handle)
expect(t, common.RunSshook("haha", "ho") == "ho", "Sshook shouldn't have anything bound to it anymore") expect(t, common.GetHookTable().Sshook("haha", "ho") == "ho", "Sshook shouldn't have anything bound to it anymore")
expect(t, common.GetHookTable().Hook("haha", "ho") == "ho", "Hook shouldn't have anything bound to it yet")
handle2 := func(inI interface{}) (out interface{}) {
return inI.(string) + "hi"
}
plugin.AddHook("hehe", handle2)
expect(t, common.GetHookTable().Hook("hehe", "ho").(string) == "hohi", "Hook didn't give hohi")
plugin.RemoveHook("hehe", handle2)
expect(t, common.GetHookTable().Hook("hehe", "ho").(string) == "ho", "Hook shouldn't have anything bound to it anymore")
// TODO: Add tests for more hook types // TODO: Add tests for more hook types
} }

View File

@ -5,6 +5,8 @@ import "testing"
// go test -v // go test -v
// TODO: Write a test for Hello World?
type MEPair struct { type MEPair struct {
Msg string Msg string
Expects string Expects string

View File

@ -81,7 +81,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt)
common.RunVhookNoreturn("forum_trow_assign", &topicItem, &forum) header.Hooks.VhookNoRet("forum_trow_assign", &topicItem, &forum)
topicList = append(topicList, &topicItem) topicList = append(topicList, &topicItem)
reqUserList[topicItem.CreatedBy] = true reqUserList[topicItem.CreatedBy] = true
reqUserList[topicItem.LastReplyBy] = true reqUserList[topicItem.LastReplyBy] = true

View File

@ -47,7 +47,7 @@ func ForumList(w http.ResponseWriter, r *http.Request, user common.User) common.
} else { } else {
forum.LastTopicTime = "" forum.LastTopicTime = ""
} }
common.RunHook("forums_frow_assign", &forum) header.Hooks.Hook("forums_frow_assign", &forum)
forumList = append(forumList, forum) forumList = append(forumList, forum)
} }
} }

View File

@ -10,7 +10,7 @@ import (
) )
func ReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, sitemID string) common.RouteError { func ReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, sitemID string) common.RouteError {
_, ferr := common.SimpleUserCheck(w, r, &user) headerLite, ferr := common.SimpleUserCheck(w, r, &user)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
@ -67,7 +67,7 @@ func ReportSubmit(w http.ResponseWriter, r *http.Request, user common.User, site
title = "Topic: " + topic.Title title = "Topic: " + topic.Title
content = topic.Content + "\n\nOriginal Post: #tid-" + strconv.Itoa(itemID) content = topic.Content + "\n\nOriginal Post: #tid-" + strconv.Itoa(itemID)
} else { } else {
_, hasHook := common.RunVhookNeedHook("report_preassign", &itemID, &itemType) _, hasHook := headerLite.Hooks.VhookNeedHook("report_preassign", &itemID, &itemType)
if hasHook { if hasHook {
return nil return nil
} }

View File

@ -186,7 +186,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
likedQueryList = append(likedQueryList, replyItem.ID) likedQueryList = append(likedQueryList, replyItem.ID)
} }
common.RunVhookNoreturn("topic_reply_row_assign", &tpage, &replyItem) header.Hooks.VhookNoRet("topic_reply_row_assign", &tpage, &replyItem)
// TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis? // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis?
tpage.ItemList = append(tpage.ItemList, replyItem) tpage.ItemList = append(tpage.ItemList, replyItem)
} }
@ -235,6 +235,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit
// TODO: Add a permission to stop certain users from using custom avatars // TODO: Add a permission to stop certain users from using custom avatars
// ? - Log username changes and put restrictions on this? // ? - Log username changes and put restrictions on this?
// TODO: Test this // TODO: Test this
// TODO: Revamp this route
func CreateTopic(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { func CreateTopic(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError {
var fid int var fid int
var err error var err error
@ -256,13 +257,13 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, user common.User, sfid
return common.NoPermissions(w, r, user) return common.NoPermissions(w, r, user)
} }
// TODO: Add a phrase for this // TODO: Add a phrase for this
header.Title = "Create Topic" header.Title = common.GetTitlePhrase("create_topic")
header.Zone = "create_topic" header.Zone = "create_topic"
// Lock this to the forum being linked? // Lock this to the forum being linked?
// Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile) // Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile)
var strictmode bool var strictmode bool
common.RunVhookNoreturn("topic_create_pre_loop", w, r, fid, &header, &user, &strictmode) header.Hooks.VhookNoRet("topic_create_pre_loop", w, r, fid, &header, &user, &strictmode)
// TODO: Re-add support for plugin_guilds // TODO: Re-add support for plugin_guilds
var forumList []common.Forum var forumList []common.Forum
@ -295,11 +296,8 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, user common.User, sfid
if forum.Name != "" && forum.Active { if forum.Name != "" && forum.Active {
fcopy := forum.Copy() fcopy := forum.Copy()
// TODO: Abstract this // TODO: Abstract this
if common.Hooks["topic_create_frow_assign"] != nil { if header.Hooks.HookSkippable("topic_create_frow_assign", &fcopy) {
// TODO: Add the skip feature to all the other row based hooks? continue
if common.RunHook("topic_create_frow_assign", &fcopy).(bool) {
continue
}
} }
forumList = append(forumList, fcopy) forumList = append(forumList, fcopy)
} }

View File

@ -3,7 +3,7 @@
<div class="rowblock rowhead"> <div class="rowblock rowhead">
<div class="rowitem"><h1>{{lang "create_topic_head"}}</h1></div> <div class="rowitem"><h1>{{lang "create_topic_head"}}</h1></div>
</div> </div>
<div class="rowblock"> <div class="rowblock the_form">
<form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form> <form id="quick_post_form" enctype="multipart/form-data" action="/topic/create/submit/?session={{.CurrentUser.Session}}" method="post"></form>
<div class="formrow real_first_child"> <div class="formrow real_first_child">
<div class="formitem formlabel"><a>{{lang "create_topic_board"}}</a></div> <div class="formitem formlabel"><a>{{lang "create_topic_board"}}</a></div>