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 }