mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
PLT-7622 Improvements to server handling of webapp plugins (#7445)
* Improvements to server handling of webapp plugins * Fix newline * Update manifest function names
This commit is contained in:
@@ -25,6 +25,8 @@ func InitPlugin() {
|
||||
BaseRoutes.Plugins.Handle("", ApiSessionRequired(getPlugins)).Methods("GET")
|
||||
BaseRoutes.Plugin.Handle("", ApiSessionRequired(removePlugin)).Methods("DELETE")
|
||||
|
||||
BaseRoutes.Plugins.Handle("/webapp", ApiHandler(getWebappPlugins)).Methods("GET")
|
||||
|
||||
}
|
||||
|
||||
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -118,3 +120,25 @@ func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*utils.Cfg.PluginSettings.Enable {
|
||||
c.Err = model.NewAppError("getWebappPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
manifests, err := c.App.GetActivePluginManifests()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
clientManifests := []*model.Manifest{}
|
||||
for _, m := range manifests {
|
||||
if m.HasClient() {
|
||||
clientManifests = append(clientManifests, m.ClientManifest())
|
||||
}
|
||||
}
|
||||
|
||||
w.Write([]byte(model.ManifestListToJson(clientManifests)))
|
||||
}
|
||||
|
||||
@@ -17,14 +17,11 @@ import (
|
||||
func TestPlugin(t *testing.T) {
|
||||
pluginDir, err := ioutil.TempDir("", "mm-plugin-test")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
os.RemoveAll(pluginDir)
|
||||
}()
|
||||
defer os.RemoveAll(pluginDir)
|
||||
|
||||
webappDir, err := ioutil.TempDir("", "mm-webapp-test")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
os.RemoveAll(webappDir)
|
||||
}()
|
||||
defer os.RemoveAll(webappDir)
|
||||
|
||||
th := SetupEnterprise().InitBasic().InitSystemAdmin()
|
||||
defer TearDown()
|
||||
@@ -50,9 +47,7 @@ func TestPlugin(t *testing.T) {
|
||||
|
||||
// Successful upload
|
||||
manifest, resp := th.SystemAdminClient.UploadPlugin(file)
|
||||
defer func() {
|
||||
os.RemoveAll("plugins/testplugin")
|
||||
}()
|
||||
defer os.RemoveAll("plugins/testplugin")
|
||||
CheckNoError(t, resp)
|
||||
|
||||
assert.Equal(t, "testplugin", manifest.Id)
|
||||
@@ -91,6 +86,19 @@ func TestPlugin(t *testing.T) {
|
||||
_, resp = th.Client.GetPlugins()
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// Successful webapp get
|
||||
manifests, resp = th.Client.GetWebappPlugins()
|
||||
CheckNoError(t, resp)
|
||||
|
||||
found = false
|
||||
for _, m := range manifests {
|
||||
if m.Id == manifest.Id {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, found)
|
||||
|
||||
// Successful remove
|
||||
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
@@ -244,7 +244,6 @@ func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
respCfg["NoAccounts"] = strconv.FormatBool(c.App.IsFirstUserAccount())
|
||||
respCfg["Plugins"] = c.App.GetPluginsForClientConfig()
|
||||
|
||||
w.Write([]byte(model.MapToJson(respCfg)))
|
||||
}
|
||||
|
||||
@@ -252,6 +252,12 @@ func (a *App) UnpackAndActivatePlugin(pluginFile io.Reader) (*model.Manifest, *m
|
||||
return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
Publish(message)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
@@ -260,10 +266,7 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.ActivePlugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
plugins := a.PluginEnv.ActivePlugins()
|
||||
|
||||
manifests := make([]*model.Manifest, len(plugins))
|
||||
for i, plugin := range plugins {
|
||||
@@ -278,6 +281,15 @@ func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins := a.PluginEnv.ActivePlugins()
|
||||
manifest := &model.Manifest{}
|
||||
for _, p := range plugins {
|
||||
if p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err := a.PluginEnv.DeactivatePlugin(id)
|
||||
if err != nil {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
@@ -288,41 +300,15 @@ func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
Publish(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Temporary WIP function/type for experimental webapp plugins
|
||||
type ClientConfigPlugin struct {
|
||||
Id string `json:"id"`
|
||||
BundlePath string `json:"bundle_path"`
|
||||
}
|
||||
|
||||
func (a *App) GetPluginsForClientConfig() string {
|
||||
if a.PluginEnv == nil || !*utils.Cfg.PluginSettings.Enable {
|
||||
return ""
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.ActivePlugins()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
pluginsConfig := []ClientConfigPlugin{}
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest.Webapp == nil {
|
||||
continue
|
||||
}
|
||||
pluginsConfig = append(pluginsConfig, ClientConfigPlugin{Id: plugin.Manifest.Id, BundlePath: plugin.Manifest.Webapp.BundlePath})
|
||||
}
|
||||
|
||||
b, err := json.Marshal(pluginsConfig)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (a *App) InitPlugins(pluginPath, webappPath string) {
|
||||
a.InitBuiltInPlugins()
|
||||
|
||||
@@ -338,6 +324,12 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Mkdir(webappPath, 0744)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
l4g.Error("failed to start up plugins: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
a.PluginEnv, err = pluginenv.New(
|
||||
pluginenv.SearchPath(pluginPath),
|
||||
pluginenv.WebappPath(webappPath),
|
||||
|
||||
@@ -62,11 +62,6 @@ func runServer(configFileLocation string) {
|
||||
l4g.Info(utils.T("mattermost.working_dir"), pwd)
|
||||
l4g.Info(utils.T("mattermost.config_file"), utils.FindConfigFile(configFileLocation))
|
||||
|
||||
// Enable developer settings if this is a "dev" build
|
||||
if model.BuildNumber == "dev" {
|
||||
*utils.Cfg.ServiceSettings.EnableDeveloper = true
|
||||
}
|
||||
|
||||
if err := utils.TestFileConnection(); err != nil {
|
||||
l4g.Error("Problem with file storage settings: " + err.Error())
|
||||
}
|
||||
@@ -79,7 +74,12 @@ func runServer(configFileLocation string) {
|
||||
if model.BuildEnterpriseReady == "true" {
|
||||
a.LoadLicense()
|
||||
}
|
||||
a.InitPlugins("plugins", "webapp/dist")
|
||||
|
||||
if webappDir, ok := utils.FindDir(model.CLIENT_DIR); ok {
|
||||
a.InitPlugins("plugins", webappDir+"/plugins")
|
||||
} else {
|
||||
l4g.Error("Unable to find webapp directory, could not initialize plugins")
|
||||
}
|
||||
|
||||
wsapi.InitRouter()
|
||||
api4.InitApi(a.Srv.Router, false)
|
||||
@@ -98,6 +98,11 @@ func runServer(configFileLocation string) {
|
||||
|
||||
app.ReloadConfig()
|
||||
|
||||
// Enable developer settings if this is a "dev" build
|
||||
if model.BuildNumber == "dev" {
|
||||
*utils.Cfg.ServiceSettings.EnableDeveloper = true
|
||||
}
|
||||
|
||||
resetStatuses(a)
|
||||
|
||||
a.StartServer()
|
||||
|
||||
@@ -3088,3 +3088,14 @@ func (c *Client4) RemovePlugin(id string) (bool, *Response) {
|
||||
return CheckStatusOK(r), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebappPlugins will return a list of plugins that the webapp should download.
|
||||
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
|
||||
func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response) {
|
||||
if r, err := c.DoApiGet(c.GetPluginsRoute()+"/webapp", ""); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return ManifestListFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
|
||||
type Manifest struct {
|
||||
Id string `json:"id" yaml:"id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Backend *ManifestBackend `json:"backend,omitempty" yaml:"backend,omitempty"`
|
||||
Webapp *ManifestWebapp `json:"webapp,omitempty" yaml:"webapp,omitempty"`
|
||||
}
|
||||
@@ -66,6 +67,19 @@ func ManifestListFromJson(data io.Reader) []*Manifest {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manifest) HasClient() bool {
|
||||
return m.Webapp != nil
|
||||
}
|
||||
|
||||
func (m *Manifest) ClientManifest() *Manifest {
|
||||
cm := new(Manifest)
|
||||
*cm = *m
|
||||
cm.Name = ""
|
||||
cm.Description = ""
|
||||
cm.Backend = nil
|
||||
return cm
|
||||
}
|
||||
|
||||
// FindManifest will find and parse the manifest in a given directory.
|
||||
//
|
||||
// In all cases other than a does-not-exist error, path is set to the path of the manifest file that was
|
||||
|
||||
@@ -129,3 +129,51 @@ func TestManifestJson(t *testing.T) {
|
||||
assert.Equal(t, newManifestList, manifestList)
|
||||
assert.Equal(t, ManifestListToJson(newManifestList), json)
|
||||
}
|
||||
|
||||
func TestManifestHasClient(t *testing.T) {
|
||||
manifest := &Manifest{
|
||||
Id: "theid",
|
||||
Backend: &ManifestBackend{
|
||||
Executable: "theexecutable",
|
||||
},
|
||||
Webapp: &ManifestWebapp{
|
||||
BundlePath: "thebundlepath",
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, manifest.HasClient())
|
||||
|
||||
manifest.Webapp = nil
|
||||
assert.False(t, manifest.HasClient())
|
||||
}
|
||||
|
||||
func TestManifestClientManifest(t *testing.T) {
|
||||
manifest := &Manifest{
|
||||
Id: "theid",
|
||||
Name: "thename",
|
||||
Description: "thedescription",
|
||||
Version: "0.0.1",
|
||||
Backend: &ManifestBackend{
|
||||
Executable: "theexecutable",
|
||||
},
|
||||
Webapp: &ManifestWebapp{
|
||||
BundlePath: "thebundlepath",
|
||||
},
|
||||
}
|
||||
|
||||
sanitized := manifest.ClientManifest()
|
||||
|
||||
assert.NotEmpty(t, sanitized.Id)
|
||||
assert.NotEmpty(t, sanitized.Version)
|
||||
assert.NotEmpty(t, sanitized.Webapp)
|
||||
assert.Empty(t, sanitized.Name)
|
||||
assert.Empty(t, sanitized.Description)
|
||||
assert.Empty(t, sanitized.Backend)
|
||||
|
||||
assert.NotEmpty(t, manifest.Id)
|
||||
assert.NotEmpty(t, manifest.Version)
|
||||
assert.NotEmpty(t, manifest.Webapp)
|
||||
assert.NotEmpty(t, manifest.Name)
|
||||
assert.NotEmpty(t, manifest.Description)
|
||||
assert.NotEmpty(t, manifest.Backend)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ const (
|
||||
WEBSOCKET_EVENT_RESPONSE = "response"
|
||||
WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added"
|
||||
WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed"
|
||||
WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE
|
||||
WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE
|
||||
)
|
||||
|
||||
type WebSocketMessage interface {
|
||||
|
||||
@@ -66,7 +66,7 @@ func (env *Environment) Plugins() ([]*model.BundleInfo, error) {
|
||||
}
|
||||
|
||||
// Returns a list of all currently active plugins within the environment.
|
||||
func (env *Environment) ActivePlugins() ([]*model.BundleInfo, error) {
|
||||
func (env *Environment) ActivePlugins() []*model.BundleInfo {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
@@ -75,7 +75,7 @@ func (env *Environment) ActivePlugins() ([]*model.BundleInfo, error) {
|
||||
activePlugins = append(activePlugins, p.BundleInfo)
|
||||
}
|
||||
|
||||
return activePlugins, nil
|
||||
return activePlugins
|
||||
}
|
||||
|
||||
// Returns the ids of the currently active plugins.
|
||||
|
||||
@@ -127,8 +127,7 @@ func TestEnvironment(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, plugins, 3)
|
||||
|
||||
activePlugins, err := env.ActivePlugins()
|
||||
assert.NoError(t, err)
|
||||
activePlugins := env.ActivePlugins()
|
||||
assert.Len(t, activePlugins, 0)
|
||||
|
||||
assert.Error(t, env.ActivatePlugin("x"))
|
||||
@@ -150,8 +149,7 @@ func TestEnvironment(t *testing.T) {
|
||||
|
||||
assert.NoError(t, env.ActivatePlugin("foo"))
|
||||
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
|
||||
activePlugins, err = env.ActivePlugins()
|
||||
assert.NoError(t, err)
|
||||
activePlugins = env.ActivePlugins()
|
||||
assert.Len(t, activePlugins, 1)
|
||||
assert.Error(t, env.ActivatePlugin("foo"))
|
||||
|
||||
|
||||
26
web/web.go
26
web/web.go
@@ -26,12 +26,17 @@ func InitWeb() {
|
||||
if *utils.Cfg.ServiceSettings.WebserverMode != "disabled" {
|
||||
staticDir, _ := utils.FindDir(model.CLIENT_DIR)
|
||||
l4g.Debug("Using client directory at %v", staticDir)
|
||||
|
||||
staticHandler := staticHandler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
|
||||
pluginHandler := pluginHandler(http.StripPrefix("/static/plugins/", http.FileServer(http.Dir(staticDir+"plugins/"))))
|
||||
|
||||
if *utils.Cfg.ServiceSettings.WebserverMode == "gzip" {
|
||||
mainrouter.PathPrefix("/static/").Handler(gziphandler.GzipHandler(staticHandler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))))
|
||||
} else {
|
||||
mainrouter.PathPrefix("/static/").Handler(staticHandler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))))
|
||||
staticHandler = gziphandler.GzipHandler(staticHandler)
|
||||
pluginHandler = gziphandler.GzipHandler(pluginHandler)
|
||||
}
|
||||
|
||||
mainrouter.PathPrefix("/static/plugins/").Handler(pluginHandler)
|
||||
mainrouter.PathPrefix("/static/").Handler(staticHandler)
|
||||
mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET")
|
||||
}
|
||||
}
|
||||
@@ -47,6 +52,21 @@ func staticHandler(handler http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func pluginHandler(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if *utils.Cfg.ServiceSettings.EnableDeveloper {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "max-age=31556926, public")
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
//map should be of minimum required browser version.
|
||||
//var browsersNotSupported string = "MSIE/11;Internet Explorer/11;Safari/9;Chrome/43;Edge/15;Firefox/52"
|
||||
//var browserMinimumSupported = [6]string{"MSIE/11", "Internet Explorer/11", "Safari/9", "Chrome/43", "Edge/15", "Firefox/52"}
|
||||
|
||||
Reference in New Issue
Block a user