// Copyright 2019 GoAdmin Core Team. All rights reserved. // Use of this source code is governed by a Apache-2.0 style // license that can be found in the LICENSE file. package engine import ( "bytes" "encoding/json" errors2 "errors" "fmt" template2 "html/template" "net/http" "runtime/debug" "strings" "sync" "github.com/GoAdminGroup/go-admin/modules/language" "github.com/GoAdminGroup/go-admin/template/icon" "github.com/GoAdminGroup/go-admin/template/types/action" "github.com/GoAdminGroup/go-admin/adapter" "github.com/GoAdminGroup/go-admin/context" "github.com/GoAdminGroup/go-admin/modules/auth" "github.com/GoAdminGroup/go-admin/modules/config" "github.com/GoAdminGroup/go-admin/modules/db" "github.com/GoAdminGroup/go-admin/modules/errors" "github.com/GoAdminGroup/go-admin/modules/logger" "github.com/GoAdminGroup/go-admin/modules/menu" "github.com/GoAdminGroup/go-admin/modules/service" "github.com/GoAdminGroup/go-admin/modules/system" "github.com/GoAdminGroup/go-admin/modules/ui" "github.com/GoAdminGroup/go-admin/plugins" "github.com/GoAdminGroup/go-admin/plugins/admin" "github.com/GoAdminGroup/go-admin/plugins/admin/models" "github.com/GoAdminGroup/go-admin/plugins/admin/modules/response" "github.com/GoAdminGroup/go-admin/plugins/admin/modules/table" "github.com/GoAdminGroup/go-admin/template" "github.com/GoAdminGroup/go-admin/template/types" ) // Engine is the core component of goAdmin. It has two attributes. // PluginList is an array of plugin. Adapter is the adapter of // web framework context and goAdmin context. The relationship of adapter and // plugin is that the adapter use the plugin which contains routers and // controller methods to inject into the framework entity and make it work. type Engine struct { PluginList plugins.Plugins Adapter adapter.WebFrameWork Services service.List NavButtons *types.Buttons config *config.Config announceLock sync.Once } // Default return the default engine instance. func Default() *Engine { engine = &Engine{ Adapter: defaultAdapter, Services: service.GetServices(), NavButtons: new(types.Buttons), } return engine } // Use enable the adapter. func (eng *Engine) Use(router interface{}) error { if eng.Adapter == nil { emptyAdapterPanic() } eng.Services.Add(auth.InitCSRFTokenSrv(eng.DefaultConnection())) eng.initSiteSetting() eng.initJumpNavButtons() eng.initPlugins() printInitMsg(language.Get("initialize success")) return eng.Adapter.Use(router, eng.PluginList) } // AddPlugins add the plugins func (eng *Engine) AddPlugins(plugs ...plugins.Plugin) *Engine { if len(plugs) == 0 { return eng } for _, plug := range plugs { eng.PluginList = eng.PluginList.Add(plug) } return eng } // AddPluginList add the plugins func (eng *Engine) AddPluginList(plugs plugins.Plugins) *Engine { if len(plugs) == 0 { return eng } for _, plug := range plugs { eng.PluginList = eng.PluginList.Add(plug) } return eng } // FindPluginByName find the register plugin by given name. func (eng *Engine) FindPluginByName(name string) (plugins.Plugin, bool) { for _, plug := range eng.PluginList { if plug.Name() == name { return plug, true } } return nil, false } // AddAuthService customize the auth logic with given callback function. func (eng *Engine) AddAuthService(processor auth.Processor) *Engine { eng.Services.Add("auth", auth.NewService(processor)) return eng } // ============================ // Config APIs // ============================ func (eng *Engine) announce() *Engine { if eng.config.Debug { eng.announceLock.Do(func() { fmt.Printf(language.Get("goadmin is now running. \nrunning in \"debug\" mode. switch to \"release\" mode in production.\n\n")) }) } return eng } // AddConfig set the global config. func (eng *Engine) AddConfig(cfg *config.Config) *Engine { return eng.setConfig(cfg).announce().initDatabase() } // setConfig set the config of engine. func (eng *Engine) setConfig(cfg *config.Config) *Engine { eng.config = config.Initialize(cfg) sysCheck, themeCheck := template.CheckRequirements() if !sysCheck { logger.Panicf(language.Get("wrong goadmin version, theme %s required goadmin version are %s"), eng.config.Theme, strings.Join(template.Default().GetRequirements(), ",")) } if !themeCheck { logger.Panicf(language.Get("wrong theme version, goadmin %s required version of theme %s is %s"), system.Version(), eng.config.Theme, strings.Join(system.RequireThemeVersion()[eng.config.Theme], ",")) } return eng } // AddConfigFromJSON set the global config from json file. func (eng *Engine) AddConfigFromJSON(path string) *Engine { cfg := config.ReadFromJson(path) return eng.setConfig(&cfg).announce().initDatabase() } // AddConfigFromYAML set the global config from yaml file. func (eng *Engine) AddConfigFromYAML(path string) *Engine { cfg := config.ReadFromYaml(path) return eng.setConfig(&cfg).announce().initDatabase() } // AddConfigFromINI set the global config from ini file. func (eng *Engine) AddConfigFromINI(path string) *Engine { cfg := config.ReadFromINI(path) return eng.setConfig(&cfg).announce().initDatabase() } // InitDatabase initialize all database connection. func (eng *Engine) initDatabase() *Engine { printInitMsg(language.Get("initialize database connections")) for driver, databaseCfg := range eng.config.Databases.GroupByDriver() { eng.Services.Add(driver, db.GetConnectionByDriver(driver).InitDB(databaseCfg)) } if defaultAdapter == nil { emptyAdapterPanic() } defaultConnection := db.GetConnection(eng.Services) defaultAdapter.SetConnection(defaultConnection) eng.Adapter.SetConnection(defaultConnection) return eng } // AddAdapter add the adapter of engine. func (eng *Engine) AddAdapter(ada adapter.WebFrameWork) *Engine { eng.Adapter = ada defaultAdapter = ada return eng } // defaultAdapter is the default adapter of engine. var defaultAdapter adapter.WebFrameWork var engine *Engine // navButtons is the default buttons in the navigation bar. var navButtons = new(types.Buttons) func emptyAdapterPanic() { logger.Panic(language.Get("adapter is nil, import the default adapter or use addadapter method add the adapter")) } // Register set default adapter of engine. func Register(ada adapter.WebFrameWork) { if ada == nil { emptyAdapterPanic() } defaultAdapter = ada } // User call the User method of defaultAdapter. func User(ctx interface{}) (models.UserModel, bool) { return defaultAdapter.User(ctx) } // User call the User method of engine adapter. func (eng *Engine) User(ctx interface{}) (models.UserModel, bool) { return eng.Adapter.User(ctx) } // ============================ // DB Connection APIs // ============================ // DB return the db connection of given driver. func (eng *Engine) DB(driver string) db.Connection { return db.GetConnectionFromService(eng.Services.Get(driver)) } // DefaultConnection return the default db connection. func (eng *Engine) DefaultConnection() db.Connection { return eng.DB(eng.config.Databases.GetDefault().Driver) } // MysqlConnection return the mysql db connection of given driver. func (eng *Engine) MysqlConnection() db.Connection { return db.GetConnectionFromService(eng.Services.Get(db.DriverMysql)) } // MssqlConnection return the mssql db connection of given driver. func (eng *Engine) MssqlConnection() db.Connection { return db.GetConnectionFromService(eng.Services.Get(db.DriverMssql)) } // PostgresqlConnection return the postgresql db connection of given driver. func (eng *Engine) PostgresqlConnection() db.Connection { return db.GetConnectionFromService(eng.Services.Get(db.DriverPostgresql)) } // SqliteConnection return the sqlite db connection of given driver. func (eng *Engine) SqliteConnection() db.Connection { return db.GetConnectionFromService(eng.Services.Get(db.DriverSqlite)) } // OceanBaseConnection return the OceanBase db connection of given driver. func (eng *Engine) OceanBaseConnection() db.Connection { return db.GetConnectionFromService(eng.Services.Get(db.DriverOceanBase)) } type ConnectionSetter func(db.Connection) // ResolveConnection resolve the specified driver connection. func (eng *Engine) ResolveConnection(setter ConnectionSetter, driver string) *Engine { setter(eng.DB(driver)) return eng } // ResolveMysqlConnection resolve the mysql connection. func (eng *Engine) ResolveMysqlConnection(setter ConnectionSetter) *Engine { eng.ResolveConnection(setter, db.DriverMysql) return eng } // ResolveMssqlConnection resolve the mssql connection. func (eng *Engine) ResolveMssqlConnection(setter ConnectionSetter) *Engine { eng.ResolveConnection(setter, db.DriverMssql) return eng } // ResolveSqliteConnection resolve the sqlite connection. func (eng *Engine) ResolveSqliteConnection(setter ConnectionSetter) *Engine { eng.ResolveConnection(setter, db.DriverSqlite) return eng } // ResolvePostgresqlConnection resolve the postgres connection. func (eng *Engine) ResolvePostgresqlConnection(setter ConnectionSetter) *Engine { eng.ResolveConnection(setter, db.DriverPostgresql) return eng } type Setter func(*Engine) // Clone copy a new Engine. func (eng *Engine) Clone(e *Engine) *Engine { e = eng return eng } // ClonedBySetter copy a new Engine by a setter callback function. func (eng *Engine) ClonedBySetter(setter Setter) *Engine { setter(eng) return eng } func (eng *Engine) deferHandler(conn db.Connection) context.Handler { return func(ctx *context.Context) { defer func(ctx *context.Context) { if user, ok := ctx.UserValue["user"].(models.UserModel); ok { var input []byte form := ctx.Request.MultipartForm if form != nil { input, _ = json.Marshal((*form).Value) } models.OperationLog().SetConn(conn).New(user.Id, ctx.Path(), ctx.Method(), ctx.LocalIP(), string(input)) } if err := recover(); err != nil { logger.Error(err) logger.Error(string(debug.Stack())) var ( errMsg string ok bool e error ) if errMsg, ok = err.(string); !ok { if e, ok = err.(error); ok { errMsg = e.Error() } } if errMsg == "" { errMsg = "system error" } if ctx.WantJSON() { response.Error(ctx, errMsg) return } eng.errorPanelHTML(ctx, new(bytes.Buffer), errors2.New(errMsg)) } }(ctx) ctx.Next() } } // wrapWithAuthMiddleware wrap a auth middleware to the given handler. func (eng *Engine) wrapWithAuthMiddleware(handler context.Handler) context.Handlers { conn := db.GetConnection(eng.Services) return []context.Handler{eng.deferHandler(conn), response.OffLineHandler, auth.Middleware(conn), handler} } // wrapWithAuthMiddleware wrap a auth middleware to the given handler. func (eng *Engine) wrap(handler context.Handler) context.Handlers { conn := db.GetConnection(eng.Services) return []context.Handler{eng.deferHandler(conn), response.OffLineHandler, handler} } // ============================ // Initialize methods // ============================ // AddNavButtons add the nav buttons. func (eng *Engine) AddNavButtons(title template2.HTML, icon string, action types.Action) *Engine { btn := types.GetNavButton(title, icon, action) *eng.NavButtons = append(*eng.NavButtons, btn) return eng } // AddNavButtonsRaw add the nav buttons. func (eng *Engine) AddNavButtonsRaw(btns ...types.Button) *Engine { *eng.NavButtons = append(*eng.NavButtons, btns...) return eng } type navJumpButtonParam struct { Exist bool Icon string BtnName string URL string Title string TitleScore string } func (eng *Engine) addJumpNavButton(param navJumpButtonParam) *Engine { if param.Exist { *eng.NavButtons = (*eng.NavButtons).AddNavButton(param.Icon, param.BtnName, action.JumpInNewTab(config.Url(param.URL), language.GetWithScope(param.Title, param.TitleScore))) } return eng } func printInitMsg(msg string) { logger.Info(msg) } func (eng *Engine) initJumpNavButtons() { printInitMsg(language.Get("initialize navigation buttons")) for _, param := range eng.initNavJumpButtonParams() { eng.addJumpNavButton(param) } navButtons = eng.NavButtons eng.Services.Add(ui.ServiceKey, ui.NewService(eng.NavButtons)) } func (eng *Engine) initPlugins() { printInitMsg(language.Get("initialize plugins")) eng.AddPlugins(admin.NewAdmin()).AddPluginList(plugins.Get()) var plugGenerators = make(table.GeneratorList) for i := range eng.PluginList { if eng.PluginList[i].Name() != "admin" { printInitMsg("--> " + eng.PluginList[i].Name()) eng.PluginList[i].InitPlugin(eng.Services) if !eng.PluginList[i].GetInfo().SkipInstallation { eng.AddGenerator("plugin_"+eng.PluginList[i].Name(), eng.PluginList[i].GetSettingPage()) } plugGenerators = plugGenerators.Combine(eng.PluginList[i].GetGenerators()) } } adm := eng.AdminPlugin().AddGenerators(plugGenerators) adm.InitPlugin(eng.Services) plugins.Add(adm) } func (eng *Engine) initNavJumpButtonParams() []navJumpButtonParam { return []navJumpButtonParam{ { Exist: !eng.config.HideConfigCenterEntrance, Icon: icon.Gear, BtnName: types.NavBtnSiteName, URL: "/info/site/edit", Title: "site setting", TitleScore: "config", }, { Exist: !eng.config.HideToolEntrance && eng.config.IsNotProductionEnvironment(), Icon: icon.Wrench, BtnName: types.NavBtnToolName, URL: "/info/generate/new", Title: "code generate tool", TitleScore: "tool", }, { Exist: !eng.config.HideAppInfoEntrance, Icon: icon.Info, BtnName: types.NavBtnInfoName, URL: "/application/info", Title: "site info", TitleScore: "system", }, { Exist: !eng.config.HidePluginEntrance, Icon: icon.Th, BtnName: types.NavBtnPlugName, URL: "/plugins", Title: "plugins", TitleScore: "plugin", }, } } func (eng *Engine) initSiteSetting() { printInitMsg(language.Get("initialize configuration")) err := eng.config.Update(models.Site(). SetConn(eng.DefaultConnection()). Init(eng.config.ToMap()). AllToMap()) if err != nil { logger.Panic(err) } eng.Services.Add("config", config.SrvWithConfig(eng.config)) errors.Init() } // ============================ // HTML Content Render APIs // ============================ // Content call the Content method of engine adapter. // If adapter is nil, it will panic. func (eng *Engine) Content(ctx interface{}, panel types.GetPanelFn) { if eng.Adapter == nil { emptyAdapterPanic() } eng.Adapter.Content(ctx, panel, eng.AdminPlugin().GetAddOperationFn(), *eng.NavButtons...) } // Content call the Content method of defaultAdapter. // If defaultAdapter is nil, it will panic. func Content(ctx interface{}, panel types.GetPanelFn) { if defaultAdapter == nil { emptyAdapterPanic() } defaultAdapter.Content(ctx, panel, engine.AdminPlugin().GetAddOperationFn(), *navButtons...) } // Data inject the route and corresponding handler to the web framework. func (eng *Engine) Data(method, url string, handler context.Handler, noAuth ...bool) { if len(noAuth) > 0 && noAuth[0] { eng.Adapter.AddHandler(method, url, eng.wrap(handler)) } else { eng.Adapter.AddHandler(method, url, eng.wrapWithAuthMiddleware(handler)) } } // HTML inject the route and corresponding handler wrapped by the given function to the web framework. func (eng *Engine) HTML(method, url string, fn types.GetPanelInfoFn, noAuth ...bool) { var handler = func(ctx *context.Context) { panel, err := fn(ctx) if err != nil { panel = template.WarningPanel(ctx, err.Error()) } eng.AdminPlugin().GetAddOperationFn()(panel.Callbacks...) var ( tmpl, tmplName = template.Default(ctx).GetTemplate(ctx.IsPjax()) user = auth.Auth(ctx) buf = new(bytes.Buffer) ) hasError := tmpl.ExecuteTemplate(buf, tmplName, types.NewPage(ctx, &types.NewPageParam{ User: user, Menu: menu.GetGlobalMenu(user, eng.Adapter.GetConnection(), ctx.Lang()).SetActiveClass(config.URLRemovePrefix(ctx.Path())), Panel: panel.GetContent(eng.config.IsProductionEnvironment()), Assets: template.GetComponentAssetImportHTML(ctx), Buttons: eng.NavButtons.CheckPermission(user), TmplHeadHTML: template.Default(ctx).GetHeadHTML(), TmplFootJS: template.Default(ctx).GetFootJS(), Iframe: ctx.IsIframe(), })) if hasError != nil { logger.Error(fmt.Sprintf("error: %s adapter content, ", eng.Adapter.Name()), hasError) } ctx.HTMLByte(http.StatusOK, buf.Bytes()) } if len(noAuth) > 0 && noAuth[0] { eng.Adapter.AddHandler(method, url, eng.wrap(handler)) } else { eng.Adapter.AddHandler(method, url, eng.wrapWithAuthMiddleware(handler)) } } // HTMLFile inject the route and corresponding handler which returns the panel content of given html file path // to the web framework. func (eng *Engine) HTMLFile(method, url, path string, data map[string]interface{}, noAuth ...bool) { var handler = func(ctx *context.Context) { cbuf := new(bytes.Buffer) t, err := template2.ParseFiles(path) if err != nil { eng.errorPanelHTML(ctx, cbuf, err) return } else if err := t.Execute(cbuf, data); err != nil { eng.errorPanelHTML(ctx, cbuf, err) return } var ( tmpl, tmplName = template.Default(ctx).GetTemplate(ctx.IsPjax()) user = auth.Auth(ctx) buf = new(bytes.Buffer) ) hasError := tmpl.ExecuteTemplate(buf, tmplName, types.NewPage(ctx, &types.NewPageParam{ User: user, Menu: menu.GetGlobalMenu(user, eng.Adapter.GetConnection(), ctx.Lang()).SetActiveClass(eng.config.URLRemovePrefix(ctx.Path())), Panel: types.Panel{ Content: template.HTML(cbuf.String()), }, Assets: template.GetComponentAssetImportHTML(ctx), Buttons: eng.NavButtons.CheckPermission(user), TmplHeadHTML: template.Default(ctx).GetHeadHTML(), TmplFootJS: template.Default(ctx).GetFootJS(), Iframe: ctx.IsIframe(), })) if hasError != nil { logger.Error(fmt.Sprintf("error: %s adapter content, ", eng.Adapter.Name()), hasError) } ctx.HTMLByte(http.StatusOK, buf.Bytes()) } if len(noAuth) > 0 && noAuth[0] { eng.Adapter.AddHandler(method, url, eng.wrap(handler)) } else { eng.Adapter.AddHandler(method, url, eng.wrapWithAuthMiddleware(handler)) } } // HTMLFiles inject the route and corresponding handler which returns the panel content of given html files path // to the web framework. func (eng *Engine) HTMLFiles(method, url string, data map[string]interface{}, files ...string) { eng.Adapter.AddHandler(method, url, eng.wrapWithAuthMiddleware(eng.htmlFilesHandler(data, files...))) } // HTMLFilesNoAuth inject the route and corresponding handler which returns the panel content of given html files path // to the web framework without auth check. func (eng *Engine) HTMLFilesNoAuth(method, url string, data map[string]interface{}, files ...string) { eng.Adapter.AddHandler(method, url, eng.wrap(eng.htmlFilesHandler(data, files...))) } // HTMLFiles inject the route and corresponding handler which returns the panel content of given html files path // to the web framework. func (eng *Engine) htmlFilesHandler(data map[string]interface{}, files ...string) context.Handler { return func(ctx *context.Context) { cbuf := new(bytes.Buffer) t, err := template2.ParseFiles(files...) if err != nil { eng.errorPanelHTML(ctx, cbuf, err) return } else if err := t.Execute(cbuf, data); err != nil { eng.errorPanelHTML(ctx, cbuf, err) return } var ( tmpl, tmplName = template.Default(ctx).GetTemplate(ctx.IsPjax()) user = auth.Auth(ctx) buf = new(bytes.Buffer) ) hasError := tmpl.ExecuteTemplate(buf, tmplName, types.NewPage(ctx, &types.NewPageParam{ User: user, Menu: menu.GetGlobalMenu(user, eng.Adapter.GetConnection(), ctx.Lang()).SetActiveClass(eng.config.URLRemovePrefix(ctx.Path())), Panel: types.Panel{ Content: template.HTML(cbuf.String()), }, Assets: template.GetComponentAssetImportHTML(ctx), Buttons: eng.NavButtons.CheckPermission(user), TmplHeadHTML: template.Default(ctx).GetHeadHTML(), TmplFootJS: template.Default(ctx).GetFootJS(), Iframe: ctx.IsIframe(), })) if hasError != nil { logger.Error(fmt.Sprintf("error: %s adapter content, ", eng.Adapter.Name()), hasError) } ctx.HTMLByte(http.StatusOK, buf.Bytes()) } } // errorPanelHTML add an error panel html to context response. func (eng *Engine) errorPanelHTML(ctx *context.Context, buf *bytes.Buffer, err error) { user := auth.Auth(ctx) tmpl, tmplName := template.Default(ctx).GetTemplate(ctx.IsPjax()) hasError := tmpl.ExecuteTemplate(buf, tmplName, types.NewPage(ctx, &types.NewPageParam{ User: user, Menu: menu.GetGlobalMenu(user, eng.Adapter.GetConnection(), ctx.Lang()).SetActiveClass(eng.config.URLRemovePrefix(ctx.Path())), Panel: template.WarningPanel(ctx, err.Error()).GetContent(eng.config.IsProductionEnvironment()), Assets: template.GetComponentAssetImportHTML(ctx), Buttons: (*eng.NavButtons).CheckPermission(user), TmplHeadHTML: template.Default(ctx).GetHeadHTML(), TmplFootJS: template.Default(ctx).GetFootJS(), Iframe: ctx.IsIframe(), })) if hasError != nil { logger.Error(fmt.Sprintf("error: %s adapter content, ", eng.Adapter.Name()), hasError) } ctx.HTMLByte(http.StatusOK, buf.Bytes()) } // ============================ // Admin Plugin APIs // ============================ // AddGenerators add the admin generators. func (eng *Engine) AddGenerators(list ...table.GeneratorList) *Engine { plug, exist := eng.FindPluginByName("admin") if exist { plug.(*admin.Admin).AddGenerators(list...) return eng } eng.PluginList = append(eng.PluginList, admin.NewAdmin(list...)) return eng } // AdminPlugin get the admin plugin. if not exist, create one. func (eng *Engine) AdminPlugin() *admin.Admin { plug, exist := eng.FindPluginByName("admin") if exist { return plug.(*admin.Admin) } adm := admin.NewAdmin() eng.PluginList = append(eng.PluginList, adm) return adm } // SetCaptcha set the captcha config. func (eng *Engine) SetCaptcha(captcha map[string]string) *Engine { eng.AdminPlugin().SetCaptcha(captcha) return eng } // SetCaptchaDriver set the captcha config with driver. func (eng *Engine) SetCaptchaDriver(driver string) *Engine { eng.AdminPlugin().SetCaptcha(map[string]string{"driver": driver}) return eng } // AddGenerator add table model generator. func (eng *Engine) AddGenerator(key string, g table.Generator) *Engine { eng.AdminPlugin().AddGenerator(key, g) return eng } // AddGlobalDisplayProcessFn call types.AddGlobalDisplayProcessFn. func (eng *Engine) AddGlobalDisplayProcessFn(f types.FieldFilterFn) *Engine { types.AddGlobalDisplayProcessFn(f) return eng } // AddDisplayFilterLimit call types.AddDisplayFilterLimit. func (eng *Engine) AddDisplayFilterLimit(limit int) *Engine { types.AddLimit(limit) return eng } // AddDisplayFilterTrimSpace call types.AddDisplayFilterTrimSpace. func (eng *Engine) AddDisplayFilterTrimSpace() *Engine { types.AddTrimSpace() return eng } // AddDisplayFilterSubstr call types.AddDisplayFilterSubstr. func (eng *Engine) AddDisplayFilterSubstr(start int, end int) *Engine { types.AddSubstr(start, end) return eng } // AddDisplayFilterToTitle call types.AddDisplayFilterToTitle. func (eng *Engine) AddDisplayFilterToTitle() *Engine { types.AddToTitle() return eng } // AddDisplayFilterToUpper call types.AddDisplayFilterToUpper. func (eng *Engine) AddDisplayFilterToUpper() *Engine { types.AddToUpper() return eng } // AddDisplayFilterToLower call types.AddDisplayFilterToLower. func (eng *Engine) AddDisplayFilterToLower() *Engine { types.AddToUpper() return eng } // AddDisplayFilterXssFilter call types.AddDisplayFilterXssFilter. func (eng *Engine) AddDisplayFilterXssFilter() *Engine { types.AddXssFilter() return eng } // AddDisplayFilterXssJsFilter call types.AddDisplayFilterXssJsFilter. func (eng *Engine) AddDisplayFilterXssJsFilter() *Engine { types.AddXssJsFilter() return eng }