github.com/kotovmak/go-admin@v1.1.1/plugins/plugins.go (about)

     1  // Copyright 2019 GoAdmin Core Team. All rights reserved.
     2  // Use of this source code is governed by a Apache-2.0 style
     3  // license that can be found in the LICENSE file.
     4  
     5  package plugins
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"errors"
    11  	template2 "html/template"
    12  	"net/http"
    13  	"plugin"
    14  	"time"
    15  
    16  	"github.com/kotovmak/go-admin/template/icon"
    17  	"github.com/kotovmak/go-admin/template/types/action"
    18  
    19  	"github.com/kotovmak/go-admin/context"
    20  	"github.com/kotovmak/go-admin/modules/auth"
    21  	"github.com/kotovmak/go-admin/modules/config"
    22  	"github.com/kotovmak/go-admin/modules/db"
    23  	"github.com/kotovmak/go-admin/modules/language"
    24  	"github.com/kotovmak/go-admin/modules/logger"
    25  	"github.com/kotovmak/go-admin/modules/menu"
    26  	"github.com/kotovmak/go-admin/modules/remote_server"
    27  	"github.com/kotovmak/go-admin/modules/service"
    28  	"github.com/kotovmak/go-admin/modules/ui"
    29  	"github.com/kotovmak/go-admin/modules/utils"
    30  	"github.com/kotovmak/go-admin/plugins/admin/models"
    31  	"github.com/kotovmak/go-admin/plugins/admin/modules/table"
    32  	"github.com/kotovmak/go-admin/template"
    33  	"github.com/kotovmak/go-admin/template/types"
    34  )
    35  
    36  // Plugin as one of the key components of goAdmin has three
    37  // methods. GetRequest return all the path registered in the
    38  // plugin. GetHandler according the url and method return the
    39  // corresponding handler. InitPlugin init the plugin which do
    40  // something like init the database and set the config and register
    41  // the routes. The Plugin must implement the three methods.
    42  type Plugin interface {
    43  	GetHandler() context.HandlerMap
    44  	InitPlugin(services service.List)
    45  	GetGenerators() table.GeneratorList
    46  	Name() string
    47  	Prefix() string
    48  	GetInfo() Info
    49  	GetIndexURL() string
    50  	GetSettingPage() table.Generator
    51  	IsInstalled() bool
    52  	Uninstall() error
    53  	Upgrade() error
    54  }
    55  
    56  type Info struct {
    57  	Title            string    `json:"title" yaml:"title" ini:"title"`
    58  	Description      string    `json:"description" yaml:"description" ini:"description"`
    59  	OldVersion       string    `json:"old_version" yaml:"old_version" ini:"old_version"`
    60  	Version          string    `json:"version" yaml:"version" ini:"version"`
    61  	Author           string    `json:"author" yaml:"author" ini:"author"`
    62  	Banners          []string  `json:"banners" yaml:"banners" ini:"banners"`
    63  	Url              string    `json:"url" yaml:"url" ini:"url"`
    64  	Cover            string    `json:"cover" yaml:"cover" ini:"cover"`
    65  	MiniCover        string    `json:"mini_cover" yaml:"mini_cover" ini:"mini_cover"`
    66  	Website          string    `json:"website" yaml:"website" ini:"website"`
    67  	Agreement        string    `json:"agreement" yaml:"agreement" ini:"agreement"`
    68  	CreateDate       time.Time `json:"create_date" yaml:"create_date" ini:"create_date"`
    69  	UpdateDate       time.Time `json:"update_date" yaml:"update_date" ini:"update_date"`
    70  	ModulePath       string    `json:"module_path" yaml:"module_path" ini:"module_path"`
    71  	Name             string    `json:"name" yaml:"name" ini:"name"`
    72  	Uuid             string    `json:"uuid" yaml:"uuid" ini:"uuid"`
    73  	Downloaded       bool      `json:"downloaded" yaml:"downloaded" ini:"downloaded"`
    74  	ExtraDownloadUrl string    `json:"extra_download_url" yaml:"extra_download_url" ini:"extra_download_url"`
    75  	Price            []string  `json:"price" yaml:"price" ini:"price"`
    76  	GoodUUIDs        []string  `json:"good_uuids" yaml:"good_uuids" ini:"good_uuids"`
    77  	GoodNum          int64     `json:"good_num" yaml:"good_num" ini:"good_num"`
    78  	CommentNum       int64     `json:"comment_num" yaml:"comment_num" ini:"comment_num"`
    79  	Order            int64     `json:"order" yaml:"order" ini:"order"`
    80  	Features         string    `json:"features" yaml:"features" ini:"features"`
    81  	Questions        []string  `json:"questions" yaml:"questions" ini:"questions"`
    82  	HasBought        bool      `json:"has_bought" yaml:"has_bought" ini:"has_bought"`
    83  	CanUpdate        bool      `json:"can_update" yaml:"can_update" ini:"can_update"`
    84  	Legal            bool      `json:"legal" yaml:"legal" ini:"legal"`
    85  	SkipInstallation bool      `json:"skip_installation" yaml:"skip_installation" ini:"skip_installation"`
    86  }
    87  
    88  func (i Info) IsFree() bool {
    89  	return len(i.Price) == 0
    90  }
    91  
    92  type Base struct {
    93  	App       *context.App
    94  	Services  service.List
    95  	Conn      db.Connection
    96  	UI        *ui.Service
    97  	PlugName  string
    98  	URLPrefix string
    99  	Info      Info
   100  }
   101  
   102  func (b *Base) InitPlugin(services service.List)   {}
   103  func (b *Base) GetGenerators() table.GeneratorList { return make(table.GeneratorList) }
   104  func (b *Base) GetHandler() context.HandlerMap     { return b.App.Handlers }
   105  func (b *Base) Name() string                       { return b.PlugName }
   106  func (b *Base) GetInfo() Info                      { return b.Info }
   107  func (b *Base) Prefix() string                     { return b.URLPrefix }
   108  func (b *Base) IsInstalled() bool                  { return false }
   109  func (b *Base) Uninstall() error                   { return nil }
   110  func (b *Base) Upgrade() error                     { return nil }
   111  func (b *Base) GetIndexURL() string                { return "" }
   112  func (b *Base) GetSettingPage() table.Generator    { return nil }
   113  
   114  func (b *Base) InitBase(srv service.List, prefix string) {
   115  	b.Services = srv
   116  	b.Conn = db.GetConnection(b.Services)
   117  	b.UI = ui.GetService(b.Services)
   118  	b.URLPrefix = prefix
   119  }
   120  
   121  func (b *Base) SetInfo(info Info) {
   122  	b.Info = info
   123  }
   124  
   125  func (b *Base) Title() string {
   126  	return language.GetWithScope(b.Info.Title, b.Name())
   127  }
   128  
   129  func (b *Base) ExecuteTmpl(ctx *context.Context, panel types.Panel, options template.ExecuteOptions) *bytes.Buffer {
   130  	return Execute(ctx, b.Conn, *b.UI.NavButtons, auth.Auth(ctx), panel, options)
   131  }
   132  
   133  func (b *Base) ExecuteTmplWithNavButtons(ctx *context.Context, panel types.Panel, btns types.Buttons,
   134  	options template.ExecuteOptions) *bytes.Buffer {
   135  	return Execute(ctx, b.Conn, btns, auth.Auth(ctx), panel, options)
   136  }
   137  
   138  func (b *Base) ExecuteTmplWithMenu(ctx *context.Context, panel types.Panel, options template.ExecuteOptions) *bytes.Buffer {
   139  	return ExecuteWithMenu(ctx, b.Conn, *b.UI.NavButtons, auth.Auth(ctx), panel, b.Name(), b.Title(), options)
   140  }
   141  
   142  func (b *Base) ExecuteTmplWithCustomMenu(ctx *context.Context, panel types.Panel, menu *menu.Menu, options template.ExecuteOptions) *bytes.Buffer {
   143  	return ExecuteWithCustomMenu(ctx, *b.UI.NavButtons, auth.Auth(ctx), panel, menu, b.Title(), options)
   144  }
   145  
   146  func (b *Base) ExecuteTmplWithMenuAndNavButtons(ctx *context.Context, panel types.Panel, menu *menu.Menu,
   147  	btns types.Buttons, options template.ExecuteOptions) *bytes.Buffer {
   148  	return ExecuteWithMenu(ctx, b.Conn, btns, auth.Auth(ctx), panel, b.Name(), b.Title(), options)
   149  }
   150  
   151  func (b *Base) NewMenu(data menu.NewMenuData) (int64, error) {
   152  	return menu.NewMenu(b.Conn, data)
   153  }
   154  
   155  func (b *Base) HTML(ctx *context.Context, panel types.Panel, options ...template.ExecuteOptions) {
   156  	buf := b.ExecuteTmpl(ctx, panel, template.GetExecuteOptions(options))
   157  	ctx.HTMLByte(http.StatusOK, buf.Bytes())
   158  }
   159  
   160  func (b *Base) HTMLCustomMenu(ctx *context.Context, panel types.Panel, menu *menu.Menu, options ...template.ExecuteOptions) {
   161  	buf := b.ExecuteTmplWithCustomMenu(ctx, panel, menu, template.GetExecuteOptions(options))
   162  	ctx.HTMLByte(http.StatusOK, buf.Bytes())
   163  }
   164  
   165  func (b *Base) HTMLMenu(ctx *context.Context, panel types.Panel, options ...template.ExecuteOptions) {
   166  	buf := b.ExecuteTmplWithMenu(ctx, panel, template.GetExecuteOptions(options))
   167  	ctx.HTMLByte(http.StatusOK, buf.Bytes())
   168  }
   169  
   170  func (b *Base) HTMLBtns(ctx *context.Context, panel types.Panel, btns types.Buttons, options ...template.ExecuteOptions) {
   171  	buf := b.ExecuteTmplWithNavButtons(ctx, panel, btns, template.GetExecuteOptions(options))
   172  	ctx.HTMLByte(http.StatusOK, buf.Bytes())
   173  }
   174  
   175  func (b *Base) HTMLMenuWithBtns(ctx *context.Context, panel types.Panel, menu *menu.Menu, btns types.Buttons, options ...template.ExecuteOptions) {
   176  	buf := b.ExecuteTmplWithMenuAndNavButtons(ctx, panel, menu, btns, template.GetExecuteOptions(options))
   177  	ctx.HTMLByte(http.StatusOK, buf.Bytes())
   178  }
   179  
   180  func (b *Base) HTMLFile(ctx *context.Context, path string, data map[string]interface{}, options ...template.ExecuteOptions) {
   181  
   182  	buf := new(bytes.Buffer)
   183  	var panel types.Panel
   184  
   185  	t, err := template2.ParseFiles(path)
   186  	if err != nil {
   187  		panel = template.WarningPanel(err.Error()).GetContent(config.IsProductionEnvironment())
   188  	} else {
   189  		if err := t.Execute(buf, data); err != nil {
   190  			panel = template.WarningPanel(err.Error()).GetContent(config.IsProductionEnvironment())
   191  		} else {
   192  			panel = types.Panel{
   193  				Content: template.HTML(buf.String()),
   194  			}
   195  		}
   196  	}
   197  
   198  	b.HTML(ctx, panel, options...)
   199  }
   200  
   201  func (b *Base) HTMLFiles(ctx *context.Context, data map[string]interface{}, files []string, options ...template.ExecuteOptions) {
   202  	buf := new(bytes.Buffer)
   203  	var panel types.Panel
   204  
   205  	t, err := template2.ParseFiles(files...)
   206  	if err != nil {
   207  		panel = template.WarningPanel(err.Error()).GetContent(config.IsProductionEnvironment())
   208  	} else {
   209  		if err := t.Execute(buf, data); err != nil {
   210  			panel = template.WarningPanel(err.Error()).GetContent(config.IsProductionEnvironment())
   211  		} else {
   212  			panel = types.Panel{
   213  				Content: template.HTML(buf.String()),
   214  			}
   215  		}
   216  	}
   217  
   218  	b.HTML(ctx, panel, options...)
   219  }
   220  
   221  type BasePlugin struct {
   222  	Base
   223  	Info      Info
   224  	IndexURL  string
   225  	Installed bool
   226  }
   227  
   228  func (b *BasePlugin) GetInfo() Info       { return b.Info }
   229  func (b *BasePlugin) Name() string        { return b.Info.Name }
   230  func (b *BasePlugin) GetIndexURL() string { return b.IndexURL }
   231  func (b *BasePlugin) IsInstalled() bool   { return b.Installed }
   232  
   233  func NewBasePluginWithInfo(info Info) Plugin {
   234  	return &BasePlugin{Info: info}
   235  }
   236  
   237  func NewBasePluginWithInfoAndIndexURL(info Info, u string, installed bool) Plugin {
   238  	return &BasePlugin{Info: info, IndexURL: u, Installed: installed}
   239  }
   240  
   241  func GetPluginsWithInfos(info []Info) Plugins {
   242  	p := make(Plugins, len(info))
   243  	for k, i := range info {
   244  		p[k] = NewBasePluginWithInfo(i)
   245  	}
   246  	return p
   247  }
   248  
   249  func LoadFromPlugin(mod string) Plugin {
   250  
   251  	plug, err := plugin.Open(mod)
   252  	if err != nil {
   253  		logger.Error("LoadFromPlugin err", err)
   254  		panic(err)
   255  	}
   256  
   257  	symPlugin, err := plug.Lookup("Plugin")
   258  	if err != nil {
   259  		logger.Error("LoadFromPlugin err", err)
   260  		panic(err)
   261  	}
   262  
   263  	var p Plugin
   264  	p, ok := symPlugin.(Plugin)
   265  	if !ok {
   266  		logger.Error("LoadFromPlugin err: unexpected type from module symbol")
   267  		panic(errors.New("LoadFromPlugin err: unexpected type from module symbol"))
   268  	}
   269  
   270  	return p
   271  }
   272  
   273  // GetHandler is a help method for Plugin GetHandler.
   274  func GetHandler(app *context.App) context.HandlerMap { return app.Handlers }
   275  
   276  func Execute(ctx *context.Context, conn db.Connection, navButtons types.Buttons, user models.UserModel,
   277  	panel types.Panel, options template.ExecuteOptions) *bytes.Buffer {
   278  	tmpl, tmplName := template.Get(config.GetTheme()).GetTemplate(ctx.IsPjax())
   279  
   280  	return template.Execute(&template.ExecuteParam{
   281  		User:       user,
   282  		TmplName:   tmplName,
   283  		Tmpl:       tmpl,
   284  		Panel:      panel,
   285  		Config:     config.Get(),
   286  		Menu:       menu.GetGlobalMenu(user, conn, ctx.Lang()).SetActiveClass(config.URLRemovePrefix(ctx.Path())),
   287  		Animation:  options.Animation,
   288  		Buttons:    navButtons.CheckPermission(user),
   289  		NoCompress: options.NoCompress,
   290  		IsPjax:     ctx.IsPjax(),
   291  		Iframe:     ctx.IsIframe(),
   292  	})
   293  }
   294  
   295  func ExecuteWithCustomMenu(ctx *context.Context,
   296  	navButtons types.Buttons,
   297  	user models.UserModel,
   298  	panel types.Panel,
   299  	menu *menu.Menu, logo string, options template.ExecuteOptions) *bytes.Buffer {
   300  
   301  	tmpl, tmplName := template.Get(config.GetTheme()).GetTemplate(ctx.IsPjax())
   302  
   303  	return template.Execute(&template.ExecuteParam{
   304  		User:       user,
   305  		TmplName:   tmplName,
   306  		Tmpl:       tmpl,
   307  		Panel:      panel,
   308  		Config:     config.Get(),
   309  		Menu:       menu,
   310  		Animation:  options.Animation,
   311  		Buttons:    navButtons.CheckPermission(user),
   312  		NoCompress: options.NoCompress,
   313  		Logo:       template2.HTML(logo),
   314  		IsPjax:     ctx.IsPjax(),
   315  		Iframe:     ctx.IsIframe(),
   316  	})
   317  }
   318  
   319  func ExecuteWithMenu(ctx *context.Context,
   320  	conn db.Connection,
   321  	navButtons types.Buttons,
   322  	user models.UserModel,
   323  	panel types.Panel,
   324  	name, logo string, options template.ExecuteOptions) *bytes.Buffer {
   325  
   326  	tmpl, tmplName := template.Get(config.GetTheme()).GetTemplate(ctx.IsPjax())
   327  
   328  	btns := options.NavDropDownButton
   329  	if btns == nil {
   330  		btns = []*types.NavDropDownItemButton{
   331  			types.GetDropDownItemButton(language.GetFromHtml("plugin setting"),
   332  				action.Jump(config.Url("/info/plugin_"+name+"/edit"))),
   333  			types.GetDropDownItemButton(language.GetFromHtml("menus manage"),
   334  				action.Jump(config.Url("/menu?__plugin_name="+name))),
   335  		}
   336  	} else {
   337  		btns = append(btns, []*types.NavDropDownItemButton{
   338  			types.GetDropDownItemButton(language.GetFromHtml("plugin setting"),
   339  				action.Jump(config.Url("/info/plugin_"+name+"/edit"))),
   340  			types.GetDropDownItemButton(language.GetFromHtml("menus manage"),
   341  				action.Jump(config.Url("/menu?__plugin_name="+name))),
   342  		}...)
   343  	}
   344  
   345  	return template.Execute(&template.ExecuteParam{
   346  		User:      user,
   347  		TmplName:  tmplName,
   348  		Tmpl:      tmpl,
   349  		Panel:     panel,
   350  		Config:    config.Get(),
   351  		Menu:      menu.GetGlobalMenu(user, conn, ctx.Lang(), name).SetActiveClass(config.URLRemovePrefix(ctx.Path())),
   352  		Animation: options.Animation,
   353  		Buttons: navButtons.Copy().
   354  			RemoveInfoNavButton().
   355  			RemoveSiteNavButton().
   356  			RemoveToolNavButton().
   357  			Add(types.GetDropDownButton("", icon.Gear, btns)).CheckPermission(user),
   358  		NoCompress: options.NoCompress,
   359  		Logo:       template2.HTML(logo),
   360  		IsPjax:     ctx.IsPjax(),
   361  		Iframe:     ctx.IsIframe(),
   362  	})
   363  }
   364  
   365  type Plugins []Plugin
   366  
   367  func (pp Plugins) Add(p Plugin) Plugins {
   368  	if !pp.Exist(p) {
   369  		pp = append(pp, p)
   370  	}
   371  	return pp
   372  }
   373  
   374  func (pp Plugins) Exist(p Plugin) bool {
   375  	for _, v := range pp {
   376  		if v.Name() == p.Name() {
   377  			return true
   378  		}
   379  	}
   380  	return false
   381  }
   382  
   383  func FindByName(name string) (Plugin, bool) {
   384  	for _, v := range pluginList {
   385  		if v.Name() == name {
   386  			return v, true
   387  		}
   388  	}
   389  	return nil, false
   390  }
   391  
   392  func FindByNameAll(name string) (Plugin, bool) {
   393  	for _, v := range allPluginList {
   394  		if v.Name() == name {
   395  			return v, true
   396  		}
   397  	}
   398  	return nil, false
   399  }
   400  
   401  var (
   402  	pluginList    = make(Plugins, 0)
   403  	allPluginList = make(Plugins, 0)
   404  )
   405  
   406  func Exist(p Plugin) bool {
   407  	return pluginList.Exist(p)
   408  }
   409  
   410  func Add(p Plugin) {
   411  	// TODO: 验证插件合法性
   412  	pluginList = pluginList.Add(p)
   413  }
   414  
   415  func GetAll(req remote_server.GetOnlineReq, token string) (Plugins, Page) {
   416  
   417  	plugs := make(Plugins, 0)
   418  	page := Page{}
   419  
   420  	res, err := remote_server.GetOnline(req, token)
   421  
   422  	if err != nil {
   423  		return plugs, page
   424  	}
   425  
   426  	var data GetOnlineRes
   427  	err = json.Unmarshal(res, &data)
   428  	if err != nil {
   429  		return plugs, page
   430  	}
   431  
   432  	if data.Code != 0 {
   433  		return plugs, page
   434  	}
   435  
   436  	plugs = GetPluginsWithInfos(data.Data.List)
   437  	page = data.Data.Page
   438  
   439  	for index, p := range plugs {
   440  		for key, value := range pluginList {
   441  			if value.Name() == p.Name() {
   442  				info := pluginList[key].GetInfo()
   443  				info.CanUpdate = utils.CompareVersion(info.Version, plugs[index].GetInfo().Version)
   444  				info.OldVersion = info.Version
   445  				info.Downloaded = true
   446  				info.Description = language.GetWithScope(info.Description, info.Name)
   447  				info.Title = language.GetWithScope(info.Title, info.Name)
   448  				info.Version = plugs[index].GetInfo().Version
   449  				plugs[index] = NewBasePluginWithInfoAndIndexURL(info, value.GetIndexURL(), value.IsInstalled())
   450  				break
   451  			}
   452  		}
   453  	}
   454  
   455  	for _, p := range plugs {
   456  		exist := false
   457  		for _, pp := range allPluginList {
   458  			if pp.Name() == p.Name() {
   459  				exist = true
   460  				break
   461  			}
   462  		}
   463  		if !exist {
   464  			allPluginList = append(allPluginList, p)
   465  		}
   466  	}
   467  
   468  	return plugs, page
   469  }
   470  
   471  func Get() Plugins {
   472  	var plugs = make(Plugins, len(pluginList))
   473  	copy(plugs, pluginList)
   474  	return plugs
   475  }
   476  
   477  type GetOnlineRes struct {
   478  	Code int              `json:"code"`
   479  	Msg  string           `json:"msg"`
   480  	Data GetOnlineResData `json:"data"`
   481  }
   482  
   483  type GetOnlineResData struct {
   484  	List    []Info `json:"list"`
   485  	Count   int    `json:"count"`
   486  	HasMore bool   `json:"has_more"`
   487  	Page    Page   `json:"page"`
   488  }
   489  
   490  type Page struct {
   491  	CSS  string `json:"css"`
   492  	HTML string `json:"html"`
   493  	JS   string `json:"js"`
   494  }