From 2628022275ef64fde95545abe4634b4bd7177844 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Fri, 15 Sep 2017 08:51:46 -0400 Subject: PLT-7622 Improvements to server handling of webapp plugins (#7445) * Improvements to server handling of webapp plugins * Fix newline * Update manifest function names --- api4/plugin.go | 24 ++++++++++++++ api4/plugin_test.go | 26 +++++++++------ api4/system.go | 1 - app/plugins.go | 62 ++++++++++++++++-------------------- cmd/platform/server.go | 17 ++++++---- model/client4.go | 11 +++++++ model/manifest.go | 18 +++++++++-- model/manifest_test.go | 48 ++++++++++++++++++++++++++++ model/websocket_message.go | 2 ++ plugin/pluginenv/environment.go | 4 +-- plugin/pluginenv/environment_test.go | 6 ++-- web/web.go | 26 +++++++++++++-- 12 files changed, 183 insertions(+), 62 deletions(-) diff --git a/api4/plugin.go b/api4/plugin.go index 5b030e71d..ac1620335 100644 --- a/api4/plugin.go +++ b/api4/plugin.go @@ -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))) +} diff --git a/api4/plugin_test.go b/api4/plugin_test.go index f92e58ea0..9cedccfe7 100644 --- a/api4/plugin_test.go +++ b/api4/plugin_test.go @@ -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) diff --git a/api4/system.go b/api4/system.go index fbfbd0ef2..b50dc97a5 100644 --- a/api4/system.go +++ b/api4/system.go @@ -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))) } diff --git a/app/plugins.go b/app/plugins.go index f00308a86..fb8182823 100644 --- a/app/plugins.go +++ b/app/plugins.go @@ -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,39 +300,13 @@ func (a *App) RemovePlugin(id string) *model.AppError { return model.NewAppError("RemovePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError) } - 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}) + if manifest.HasClient() { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil) + message.Add("manifest", manifest.ClientManifest()) + Publish(message) } - b, err := json.Marshal(pluginsConfig) - if err != nil { - return "" - } - - return string(b) + return nil } func (a *App) InitPlugins(pluginPath, webappPath string) { @@ -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), diff --git a/cmd/platform/server.go b/cmd/platform/server.go index 15c80134c..a8e724f58 100644 --- a/cmd/platform/server.go +++ b/cmd/platform/server.go @@ -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() diff --git a/model/client4.go b/model/client4.go index c06975697..44c4cf6c9 100644 --- a/model/client4.go +++ b/model/client4.go @@ -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) + } +} diff --git a/model/manifest.go b/model/manifest.go index e61ccc8ad..b466660af 100644 --- a/model/manifest.go +++ b/model/manifest.go @@ -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 diff --git a/model/manifest_test.go b/model/manifest_test.go index 46975cf47..1ec43a217 100644 --- a/model/manifest_test.go +++ b/model/manifest_test.go @@ -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) +} diff --git a/model/websocket_message.go b/model/websocket_message.go index 6b8c03427..6c55da6f0 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -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 { diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go index e4a7f1b3b..805b33e66 100644 --- a/plugin/pluginenv/environment.go +++ b/plugin/pluginenv/environment.go @@ -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. diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go index f24ef8d3d..c11644b35 100644 --- a/plugin/pluginenv/environment_test.go +++ b/plugin/pluginenv/environment_test.go @@ -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")) diff --git a/web/web.go b/web/web.go index c883c750d..f74c73cde 100644 --- a/web/web.go +++ b/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"} -- cgit v1.2.3-1-g7c22