github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/extend.go (about)

     1  /*
     2  *
     3  * Gosora Plugin System
     4  * Copyright Azareal 2016 - 2021
     5  *
     6   */
     7  package common
     8  
     9  // TODO: Break this file up into multiple files to make it easier to maintain
    10  import (
    11  	"database/sql"
    12  	"errors"
    13  	"log"
    14  	"net/http"
    15  	"sync"
    16  	"sync/atomic"
    17  
    18  	qgen "github.com/Azareal/Gosora/query_gen"
    19  )
    20  
    21  var ErrPluginNotInstallable = errors.New("This plugin is not installable")
    22  
    23  type PluginList map[string]*Plugin
    24  
    25  // TODO: Have a proper store rather than a map?
    26  var Plugins PluginList = make(map[string]*Plugin)
    27  
    28  func (l PluginList) Add(pl *Plugin) {
    29  	buildPlugin(pl)
    30  	l[pl.UName] = pl
    31  }
    32  
    33  func buildPlugin(pl *Plugin) {
    34  	pl.Installable = (pl.Install != nil)
    35  	/*
    36  		The Active field should never be altered by a plugin. It's used internally by the software to determine whether an admin has enabled a plugin or not and whether to run it. This will be overwritten by the user's preference.
    37  	*/
    38  	pl.Active = false
    39  	pl.Installed = false
    40  	pl.Hooks = make(map[string]int)
    41  	pl.Data = nil
    42  }
    43  
    44  var hookTableBox atomic.Value
    45  
    46  // ! HookTable is a work in progress, do not use it yet
    47  // TODO: Test how fast it is to indirect hooks off the hook table as opposed to using them normally or using an interface{} for the hooks
    48  // TODO: Can we filter the HookTable for each request down to only hooks the request actually uses?
    49  // TODO: Make the RunXHook functions methods on HookTable
    50  // 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
    51  type HookTable struct {
    52  	//Hooks           map[string][]func(interface{}) interface{}
    53  	HooksNoRet      map[string][]func(interface{})
    54  	HooksSkip       map[string][]func(interface{}) bool
    55  	Vhooks          map[string]func(...interface{}) interface{}
    56  	VhookSkippable_ map[string]func(...interface{}) (bool, RouteError)
    57  	Sshooks         map[string][]func(string) string
    58  	PreRenderHooks  map[string][]func(http.ResponseWriter, *http.Request, *User, interface{}) bool
    59  
    60  	// For future use:
    61  	//messageHooks map[string][]func(Message, PageInt, ...interface{}) interface{}
    62  }
    63  
    64  func init() {
    65  	RebuildHookTable()
    66  }
    67  
    68  // For extend.go use only, access this via GetHookTable() elsewhere
    69  var hookTable = &HookTable{
    70  	//map[string][]func(interface{}) interface{}{},
    71  	map[string][]func(interface{}){
    72  		"forums_frow_assign": nil, //hg
    73  	},
    74  	map[string][]func(interface{}) bool{
    75  		"topic_create_frow_assign": nil, //hg
    76  	},
    77  	map[string]func(...interface{}) interface{}{
    78  		//"convo_post_update":nil,
    79  		//"convo_post_create":nil,
    80  
    81  		///"forum_trow_assign":       nil,
    82  		"topics_topic_row_assign": nil,
    83  		//"topics_user_row_assign": nil,
    84  		"topic_reply_row_assign": nil,
    85  		"create_group_preappend": nil, // What is this? Investigate!
    86  		"topic_create_pre_loop":  nil,
    87  
    88  		"router_end": nil,
    89  	},
    90  	map[string]func(...interface{}) (bool, RouteError){
    91  		"simple_forum_check_pre_perms": nil, //hg
    92  		"forum_check_pre_perms":        nil, //hg
    93  
    94  		"route_topic_list_start":            nil,
    95  		"route_topic_list_mostviewed_start": nil,
    96  		"route_forum_list_start":            nil,
    97  		"route_attach_start":                nil,
    98  		"route_attach_post_get":             nil,
    99  
   100  		"action_end_create_topic":  nil,
   101  		"action_end_edit_topic":    nil,
   102  		"action_end_delete_topic":  nil,
   103  		"action_end_lock_topic":    nil,
   104  		"action_end_unlock_topic":  nil,
   105  		"action_end_stick_topic":   nil,
   106  		"action_end_unstick_topic": nil,
   107  		"action_end_move_topic":    nil,
   108  		"action_end_like_topic":    nil,
   109  		"action_end_unlike_topic":  nil,
   110  
   111  		"action_end_create_reply":             nil,
   112  		"action_end_edit_reply":               nil,
   113  		"action_end_delete_reply":             nil,
   114  		"action_end_add_attach_to_reply":      nil,
   115  		"action_end_remove_attach_from_reply": nil,
   116  
   117  		"action_end_like_reply":   nil,
   118  		"action_end_unlike_reply": nil,
   119  
   120  		"action_end_ban_user":      nil,
   121  		"action_end_unban_user":    nil,
   122  		"action_end_activate_user": nil,
   123  
   124  		"router_after_filters": nil,
   125  		"router_pre_route":     nil,
   126  
   127  		"tasks_tick_topic_list": nil,
   128  		"tasks_tick_widget_wol": nil,
   129  
   130  		"counters_perf_tick_row": nil,
   131  	},
   132  	map[string][]func(string) string{
   133  		"preparse_preassign":  nil,
   134  		"parse_assign":        nil,
   135  		"topic_ogdesc_assign": nil,
   136  	},
   137  	nil,
   138  	//nil,
   139  }
   140  var hookTableUpdateMutex sync.Mutex
   141  
   142  func RebuildHookTable() {
   143  	hookTableUpdateMutex.Lock()
   144  	defer hookTableUpdateMutex.Unlock()
   145  	unsafeRebuildHookTable()
   146  }
   147  
   148  func unsafeRebuildHookTable() {
   149  	ihookTable := new(HookTable)
   150  	*ihookTable = *hookTable
   151  	hookTableBox.Store(ihookTable)
   152  }
   153  
   154  func GetHookTable() *HookTable {
   155  	return hookTableBox.Load().(*HookTable)
   156  }
   157  
   158  // 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?
   159  /*func (t *HookTable) Hook(name string, data interface{}) interface{} {
   160  	for _, hook := range t.Hooks[name] {
   161  		data = hook(data)
   162  	}
   163  	return data
   164  }*/
   165  
   166  func (t *HookTable) HookNoRet(name string, data interface{}) {
   167  	for _, hook := range t.HooksNoRet[name] {
   168  		hook(data)
   169  	}
   170  }
   171  
   172  // To cover the case in routes/topic.go's CreateTopic route, we could probably obsolete this use and replace it
   173  func (t *HookTable) HookSkip(name string, data interface{}) (skip bool) {
   174  	for _, hook := range t.HooksSkip[name] {
   175  		if skip = hook(data); skip {
   176  			break
   177  		}
   178  	}
   179  	return skip
   180  }
   181  
   182  // Hooks with a variable number of arguments
   183  // TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn?
   184  func (t *HookTable) Vhook(name string, data ...interface{}) interface{} {
   185  	if hook := t.Vhooks[name]; hook != nil {
   186  		return hook(data...)
   187  	}
   188  	return nil
   189  }
   190  
   191  func (t *HookTable) VhookNoRet(name string, data ...interface{}) {
   192  	if hook := t.Vhooks[name]; hook != nil {
   193  		_ = hook(data...)
   194  	}
   195  }
   196  
   197  // TODO: Find a better way of doing this
   198  func (t *HookTable) VhookNeedHook(name string, data ...interface{}) (ret interface{}, hasHook bool) {
   199  	if hook := t.Vhooks[name]; hook != nil {
   200  		return hook(data...), true
   201  	}
   202  	return nil, false
   203  }
   204  
   205  // Hooks with a variable number of arguments and return values for skipping the parent function and propagating an error upwards
   206  func (t *HookTable) VhookSkippable(name string, data ...interface{}) (bool, RouteError) {
   207  	if hook := t.VhookSkippable_[name]; hook != nil {
   208  		return hook(data...)
   209  	}
   210  	return false, nil
   211  }
   212  
   213  /*func VhookSkippableTest(t *HookTable, name string, data ...interface{}) (bool, RouteError) {
   214  	if hook := t.VhookSkippable_[name]; hook != nil {
   215  		return hook(data...)
   216  	}
   217  	return false, nil
   218  }
   219  
   220  func forum_check_pre_perms_hook(t *HookTable, w http.ResponseWriter, r *http.Request, u *User, fid *int, h *Header) (bool, RouteError) {
   221  	hook := t.VhookSkippable_["forum_check_pre_perms"]
   222  	if hook != nil {
   223  		return hook(w, r, u, fid, h)
   224  	}
   225  	return false, nil
   226  }*/
   227  
   228  // Hooks which take in and spit out a string. This is usually used for parser components
   229  // Trying to get a teeny bit of type-safety where-ever possible, especially for such a critical set of hooks
   230  func (t *HookTable) Sshook(name, data string) string {
   231  	for _, hook := range t.Sshooks[name] {
   232  		data = hook(data)
   233  	}
   234  	return data
   235  }
   236  
   237  //var vhookErrorable = map[string]func(...interface{}) (interface{}, RouteError){}
   238  
   239  var taskHooks = map[string][]func() error{
   240  	"before_half_second_tick":    nil,
   241  	"after_half_second_tick":     nil,
   242  	"before_second_tick":         nil,
   243  	"after_second_tick":          nil,
   244  	"before_fifteen_minute_tick": nil,
   245  	"after_fifteen_minute_tick":  nil,
   246  	"before_shutdown_tick":       nil,
   247  	"after_shutdown_tick":        nil,
   248  }
   249  
   250  // Coming Soon:
   251  type Message interface {
   252  	ID() int
   253  	Poster() int
   254  	Contents() string
   255  	ParsedContents() string
   256  }
   257  
   258  // While the idea is nice, this might result in too much code duplication, as we have seventy billion page structs, what else could we do to get static typing with these in plugins?
   259  type PageInt interface {
   260  	Title() string
   261  	Header() *Header
   262  	CurrentUser() *User
   263  	GetExtData(name string) interface{}
   264  	SetExtData(name string, contents interface{})
   265  }
   266  
   267  // Coming Soon:
   268  var messageHooks = map[string][]func(Message, PageInt, ...interface{}) interface{}{
   269  	"topic_reply_row_assign": nil,
   270  }
   271  
   272  // The hooks which run before the template is rendered for a route
   273  var PreRenderHooks = map[string][]func(http.ResponseWriter, *http.Request, *User, interface{}) bool{
   274  	"pre_render": nil,
   275  
   276  	"pre_render_forums":       nil,
   277  	"pre_render_forum":        nil,
   278  	"pre_render_topics":       nil,
   279  	"pre_render_topic":        nil,
   280  	"pre_render_profile":      nil,
   281  	"pre_render_custom_page":  nil,
   282  	"pre_render_tmpl_page":    nil,
   283  	"pre_render_overview":     nil,
   284  	"pre_render_create_topic": nil,
   285  
   286  	"pre_render_account_own_edit":           nil,
   287  	"pre_render_account_own_edit_password":  nil,
   288  	"pre_render_account_own_edit_mfa":       nil,
   289  	"pre_render_account_own_edit_mfa_setup": nil,
   290  	"pre_render_account_own_edit_email":     nil,
   291  	"pre_render_level_list":                 nil,
   292  	"pre_render_login":                      nil,
   293  	"pre_render_login_mfa_verify":           nil,
   294  	"pre_render_register":                   nil,
   295  	"pre_render_ban":                        nil,
   296  	"pre_render_ip_search":                  nil,
   297  
   298  	"pre_render_panel_dashboard":        nil,
   299  	"pre_render_panel_forums":           nil,
   300  	"pre_render_panel_delete_forum":     nil,
   301  	"pre_render_panel_forum_edit":       nil,
   302  	"pre_render_panel_forum_edit_perms": nil,
   303  
   304  	"pre_render_panel_analytics_views":          nil,
   305  	"pre_render_panel_analytics_routes":         nil,
   306  	"pre_render_panel_analytics_agents":         nil,
   307  	"pre_render_panel_analytics_systems":        nil,
   308  	"pre_render_panel_analytics_referrers":      nil,
   309  	"pre_render_panel_analytics_route_views":    nil,
   310  	"pre_render_panel_analytics_agent_views":    nil,
   311  	"pre_render_panel_analytics_system_views":   nil,
   312  	"pre_render_panel_analytics_referrer_views": nil,
   313  
   314  	"pre_render_panel_settings":          nil,
   315  	"pre_render_panel_setting":           nil,
   316  	"pre_render_panel_word_filters":      nil,
   317  	"pre_render_panel_word_filters_edit": nil,
   318  	"pre_render_panel_plugins":           nil,
   319  	"pre_render_panel_users":             nil,
   320  	"pre_render_panel_user_edit":         nil,
   321  	"pre_render_panel_groups":            nil,
   322  	"pre_render_panel_group_edit":        nil,
   323  	"pre_render_panel_group_edit_perms":  nil,
   324  	"pre_render_panel_themes":            nil,
   325  	"pre_render_panel_modlogs":           nil,
   326  
   327  	"pre_render_error": nil, // Note: This hook isn't run for a few errors whose templates are computed at startup and reused, such as InternalError. This hook is also not available in JS mode.
   328  	// ^-- I don't know if it's run for InternalError, but it isn't computed at startup anymore
   329  	"pre_render_security_error": nil,
   330  }
   331  
   332  // ? - Should we make this an interface which plugins implement instead?
   333  // Plugin is a struct holding the metadata for a plugin, along with a few of it's primary handlers.
   334  type Plugin struct {
   335  	UName       string
   336  	Name        string
   337  	Author      string
   338  	URL         string
   339  	Settings    string
   340  	Active      bool
   341  	Tag         string
   342  	Type        string
   343  	Installable bool
   344  	Installed   bool
   345  
   346  	Init       func(pl *Plugin) error
   347  	Activate   func(pl *Plugin) error
   348  	Deactivate func(pl *Plugin) // TODO: We might want to let this return an error?
   349  	Install    func(pl *Plugin) error
   350  	Uninstall  func(pl *Plugin) error // TODO: I'm not sure uninstall is implemented
   351  
   352  	Hooks map[string]int // Active hooks
   353  	Meta  PluginMetaData
   354  	Data  interface{} // Usually used for hosting the VMs / reusable elements of non-native plugins
   355  }
   356  
   357  type PluginMetaData struct {
   358  	Hooks []string
   359  	//StaticHooks map[string]string
   360  }
   361  
   362  func (pl *Plugin) BypassActive() (active bool, err error) {
   363  	err = extendStmts.isActive.QueryRow(pl.UName).Scan(&active)
   364  	if err != nil && err != sql.ErrNoRows {
   365  		return false, err
   366  	}
   367  	return active, nil
   368  }
   369  
   370  func (pl *Plugin) InDatabase() (exists bool, err error) {
   371  	var sink bool
   372  	err = extendStmts.isActive.QueryRow(pl.UName).Scan(&sink)
   373  	if err != nil && err != sql.ErrNoRows {
   374  		return false, err
   375  	}
   376  	return err == nil, nil
   377  }
   378  
   379  // TODO: Silently add to the database, if it doesn't exist there rather than forcing users to call AddToDatabase instead?
   380  func (pl *Plugin) SetActive(active bool) (err error) {
   381  	_, err = extendStmts.setActive.Exec(active, pl.UName)
   382  	if err == nil {
   383  		pl.Active = active
   384  	}
   385  	return err
   386  }
   387  
   388  // TODO: Silently add to the database, if it doesn't exist there rather than forcing users to call AddToDatabase instead?
   389  func (pl *Plugin) SetInstalled(installed bool) (err error) {
   390  	if !pl.Installable {
   391  		return ErrPluginNotInstallable
   392  	}
   393  	_, err = extendStmts.setInstalled.Exec(installed, pl.UName)
   394  	if err == nil {
   395  		pl.Installed = installed
   396  	}
   397  	return err
   398  }
   399  
   400  func (pl *Plugin) AddToDatabase(active, installed bool) (err error) {
   401  	_, err = extendStmts.add.Exec(pl.UName, active, installed)
   402  	if err == nil {
   403  		pl.Active = active
   404  		pl.Installed = installed
   405  	}
   406  	return err
   407  }
   408  
   409  type ExtendStmts struct {
   410  	getPlugins *sql.Stmt
   411  
   412  	isActive     *sql.Stmt
   413  	setActive    *sql.Stmt
   414  	setInstalled *sql.Stmt
   415  	add          *sql.Stmt
   416  }
   417  
   418  var extendStmts ExtendStmts
   419  
   420  func init() {
   421  	DbInits.Add(func(acc *qgen.Accumulator) error {
   422  		pl := "plugins"
   423  		extendStmts = ExtendStmts{
   424  			getPlugins: acc.Select(pl).Columns("uname,active,installed").Prepare(),
   425  
   426  			isActive:     acc.Select(pl).Columns("active").Where("uname=?").Prepare(),
   427  			setActive:    acc.Update(pl).Set("active=?").Where("uname=?").Prepare(),
   428  			setInstalled: acc.Update(pl).Set("installed=?").Where("uname=?").Prepare(),
   429  			add:          acc.Insert(pl).Columns("uname,active,installed").Fields("?,?,?").Prepare(),
   430  		}
   431  		return acc.FirstError()
   432  	})
   433  }
   434  
   435  func InitExtend() error {
   436  	err := InitPluginLangs()
   437  	if err != nil {
   438  		return err
   439  	}
   440  	return Plugins.Load()
   441  }
   442  
   443  // Load polls the database to see which plugins have been activated and which have been installed
   444  func (l PluginList) Load() error {
   445  	rows, err := extendStmts.getPlugins.Query()
   446  	if err != nil {
   447  		return err
   448  	}
   449  	defer rows.Close()
   450  
   451  	var uname string
   452  	var active, installed bool
   453  	for rows.Next() {
   454  		err = rows.Scan(&uname, &active, &installed)
   455  		if err != nil {
   456  			return err
   457  		}
   458  
   459  		// Was the plugin deleted at some point?
   460  		pl, ok := l[uname]
   461  		if !ok {
   462  			continue
   463  		}
   464  		pl.Active = active
   465  		pl.Installed = installed
   466  		l[uname] = pl
   467  	}
   468  	return rows.Err()
   469  }
   470  
   471  // ? - Is this racey?
   472  // TODO: Generate the cases in this switch
   473  func (pl *Plugin) AddHook(name string, hInt interface{}) {
   474  	hookTableUpdateMutex.Lock()
   475  	defer hookTableUpdateMutex.Unlock()
   476  
   477  	switch h := hInt.(type) {
   478  	/*case func(interface{}) interface{}:
   479  	if len(hookTable.Hooks[name]) == 0 {
   480  		hookTable.Hooks[name] = []func(interface{}) interface{}{}
   481  	}
   482  	hookTable.Hooks[name] = append(hookTable.Hooks[name], h)
   483  	pl.Hooks[name] = len(hookTable.Hooks[name]) - 1*/
   484  	case func(interface{}):
   485  		if len(hookTable.HooksNoRet[name]) == 0 {
   486  			hookTable.HooksNoRet[name] = []func(interface{}){}
   487  		}
   488  		hookTable.HooksNoRet[name] = append(hookTable.HooksNoRet[name], h)
   489  		pl.Hooks[name] = len(hookTable.HooksNoRet[name]) - 1
   490  	case func(interface{}) bool:
   491  		if len(hookTable.HooksSkip[name]) == 0 {
   492  			hookTable.HooksSkip[name] = []func(interface{}) bool{}
   493  		}
   494  		hookTable.HooksSkip[name] = append(hookTable.HooksSkip[name], h)
   495  		pl.Hooks[name] = len(hookTable.HooksSkip[name]) - 1
   496  	case func(string) string:
   497  		if len(hookTable.Sshooks[name]) == 0 {
   498  			hookTable.Sshooks[name] = []func(string) string{}
   499  		}
   500  		hookTable.Sshooks[name] = append(hookTable.Sshooks[name], h)
   501  		pl.Hooks[name] = len(hookTable.Sshooks[name]) - 1
   502  	case func(http.ResponseWriter, *http.Request, *User, interface{}) bool:
   503  		if len(PreRenderHooks[name]) == 0 {
   504  			PreRenderHooks[name] = []func(http.ResponseWriter, *http.Request, *User, interface{}) bool{}
   505  		}
   506  		PreRenderHooks[name] = append(PreRenderHooks[name], h)
   507  		pl.Hooks[name] = len(PreRenderHooks[name]) - 1
   508  	case func() error: // ! We might want a more generic name, as we might use this signature for things other than tasks hooks
   509  		if len(taskHooks[name]) == 0 {
   510  			taskHooks[name] = []func() error{}
   511  		}
   512  		taskHooks[name] = append(taskHooks[name], h)
   513  		pl.Hooks[name] = len(taskHooks[name]) - 1
   514  	case func(...interface{}) interface{}:
   515  		hookTable.Vhooks[name] = h
   516  		pl.Hooks[name] = 0
   517  	case func(...interface{}) (bool, RouteError):
   518  		hookTable.VhookSkippable_[name] = h
   519  		pl.Hooks[name] = 0
   520  	default:
   521  		panic("I don't recognise this kind of handler!") // Should this be an error for the plugin instead of a panic()?
   522  	}
   523  	// TODO: Do this once during plugin activation / deactivation rather than doing it for each hook
   524  	unsafeRebuildHookTable()
   525  }
   526  
   527  // ? - Is this racey?
   528  // TODO: Generate the cases in this switch
   529  func (pl *Plugin) RemoveHook(name string, hInt interface{}) {
   530  	hookTableUpdateMutex.Lock()
   531  	defer hookTableUpdateMutex.Unlock()
   532  
   533  	key, ok := pl.Hooks[name]
   534  	if !ok {
   535  		panic("handler not registered as hook")
   536  	}
   537  
   538  	switch hInt.(type) {
   539  	/*case func(interface{}) interface{}:
   540  	hook := hookTable.Hooks[name]
   541  	if len(hook) == 1 {
   542  		hook = []func(interface{}) interface{}{}
   543  	} else {
   544  		hook = append(hook[:key], hook[key+1:]...)
   545  	}
   546  	hookTable.Hooks[name] = hook*/
   547  	case func(interface{}):
   548  		hook := hookTable.HooksNoRet[name]
   549  		if len(hook) == 1 {
   550  			hook = []func(interface{}){}
   551  		} else {
   552  			hook = append(hook[:key], hook[key+1:]...)
   553  		}
   554  		hookTable.HooksNoRet[name] = hook
   555  	case func(interface{}) bool:
   556  		hook := hookTable.HooksSkip[name]
   557  		if len(hook) == 1 {
   558  			hook = []func(interface{}) bool{}
   559  		} else {
   560  			hook = append(hook[:key], hook[key+1:]...)
   561  		}
   562  		hookTable.HooksSkip[name] = hook
   563  	case func(string) string:
   564  		hook := hookTable.Sshooks[name]
   565  		if len(hook) == 1 {
   566  			hook = []func(string) string{}
   567  		} else {
   568  			hook = append(hook[:key], hook[key+1:]...)
   569  		}
   570  		hookTable.Sshooks[name] = hook
   571  	case func(http.ResponseWriter, *http.Request, *User, interface{}) bool:
   572  		hook := PreRenderHooks[name]
   573  		if len(hook) == 1 {
   574  			hook = []func(http.ResponseWriter, *http.Request, *User, interface{}) bool{}
   575  		} else {
   576  			hook = append(hook[:key], hook[key+1:]...)
   577  		}
   578  		PreRenderHooks[name] = hook
   579  	case func() error:
   580  		hook := taskHooks[name]
   581  		if len(hook) == 1 {
   582  			hook = []func() error{}
   583  		} else {
   584  			hook = append(hook[:key], hook[key+1:]...)
   585  		}
   586  		taskHooks[name] = hook
   587  	case func(...interface{}) interface{}:
   588  		delete(hookTable.Vhooks, name)
   589  	case func(...interface{}) (bool, RouteError):
   590  		delete(hookTable.VhookSkippable_, name)
   591  	default:
   592  		panic("I don't recognise this kind of handler!") // Should this be an error for the plugin instead of a panic()?
   593  	}
   594  	delete(pl.Hooks, name)
   595  	// TODO: Do this once during plugin activation / deactivation rather than doing it for each hook
   596  	unsafeRebuildHookTable()
   597  }
   598  
   599  // TODO: Add a HasHook method to complete the AddHook, RemoveHook, etc. set?
   600  
   601  var PluginsInited = false
   602  
   603  func InitPlugins() {
   604  	for name, body := range Plugins {
   605  		log.Printf("Added plugin '%s'", name)
   606  		if body.Active {
   607  			log.Printf("Initialised plugin '%s'", name)
   608  			if body.Init != nil {
   609  				if err := body.Init(body); err != nil {
   610  					log.Print(err)
   611  				}
   612  			} else {
   613  				log.Printf("Plugin '%s' doesn't have an initialiser.", name)
   614  			}
   615  		}
   616  	}
   617  	PluginsInited = true
   618  }
   619  
   620  // ? - Are the following functions racey?
   621  func RunTaskHook(name string) error {
   622  	for _, hook := range taskHooks[name] {
   623  		if e := hook(); e != nil {
   624  			return e
   625  		}
   626  	}
   627  	return nil
   628  }
   629  
   630  func RunPreRenderHook(name string, w http.ResponseWriter, r *http.Request, u *User, data interface{}) (halt bool) {
   631  	// This hook runs on ALL PreRender hooks
   632  	preRenderHooks, ok := PreRenderHooks["pre_render"]
   633  	if ok {
   634  		for _, hook := range preRenderHooks {
   635  			if hook(w, r, u, data) {
   636  				return true
   637  			}
   638  		}
   639  	}
   640  
   641  	// The actual PreRender hook
   642  	preRenderHooks, ok = PreRenderHooks[name]
   643  	if ok {
   644  		for _, hook := range preRenderHooks {
   645  			if hook(w, r, u, data) {
   646  				return true
   647  			}
   648  		}
   649  	}
   650  	return false
   651  }