Commit 26280222 authored by Joram Wilander's avatar Joram Wilander Committed by GitHub

PLT-7622 Improvements to server handling of webapp plugins (#7445)

* Improvements to server handling of webapp plugins

* Fix newline

* Update manifest function names
parent 2a6cd44f
......@@ -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,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),
......
......@@ -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,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"}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment