diff --git a/app/plugin_api.go b/app/plugin_api.go index 04da1e7ce1..790a14da21 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -7,6 +7,8 @@ import ( "bytes" "encoding/json" "fmt" + "io" + "io/ioutil" "net/http" "path/filepath" "strings" @@ -655,6 +657,19 @@ func (api *PluginAPI) GetPluginStatus(id string) (*model.PluginStatus, *model.Ap return api.app.GetPluginStatus(id) } +func (api *PluginAPI) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) { + if !*api.app.Config().PluginSettings.Enable || !*api.app.Config().PluginSettings.EnableUploads { + return nil, model.NewAppError("installPlugin", "app.plugin.upload_disabled.app_error", nil, "", http.StatusNotImplemented) + } + + fileBuffer, err := ioutil.ReadAll(file) + if err != nil { + return nil, model.NewAppError("InstallPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest) + } + + return api.app.InstallPlugin(bytes.NewReader(fileBuffer), replace) +} + // KV Store Section func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError { diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index d913c03142..34ee7af700 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -21,6 +21,7 @@ import ( "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/services/mailservice" "github.com/mattermost/mattermost-server/utils" + "github.com/mattermost/mattermost-server/utils/fileutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -684,6 +685,43 @@ func TestPluginAPIGetPlugins(t *testing.T) { assert.Equal(t, pluginManifests, plugins) } +func TestPluginAPIInstallPlugin(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + path, _ := fileutils.FindDir("tests") + tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + + _, err = api.InstallPlugin(bytes.NewReader(tarData), true) + assert.NotNil(t, err, "should not allow upload if upload disabled") + assert.Equal(t, err.Error(), "installPlugin: Plugins and/or plugin uploads have been disabled., ") + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableUploads = true + }) + + manifest, err := api.InstallPlugin(bytes.NewReader(tarData), true) + defer os.RemoveAll("plugins/testplugin") + require.Nil(t, err) + assert.Equal(t, "testplugin", manifest.Id) + + // Successfully installed + pluginsResp, err := api.GetPlugins() + require.Nil(t, err) + + found := false + for _, m := range pluginsResp { + if m.Id == manifest.Id { + found = true + } + } + + assert.True(t, found) +} + func TestPluginAPIGetTeamIcon(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/plugin/api.go b/plugin/api.go index 0bfe69cc84..4a21a331d4 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -4,6 +4,8 @@ package plugin import ( + "io" + plugin "github.com/hashicorp/go-plugin" "github.com/mattermost/mattermost-server/model" ) @@ -557,6 +559,12 @@ type API interface { // Minimum server version: 5.6 GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) + // InstallPlugin will upload another plugin with tar.gz file. + // Previous version will be replaced on replace true. + // + // Minimum server version: 5.18 + InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) + // KV Store Section // KVSet stores a key-value pair, unique per plugin. diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index e1bd6165ff..43ba11b2f0 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -8,6 +8,7 @@ package plugin import ( "fmt" + "io" "log" "github.com/mattermost/mattermost-server/mlog" @@ -3488,6 +3489,36 @@ func (s *apiRPCServer) GetPluginStatus(args *Z_GetPluginStatusArgs, returns *Z_G return nil } +type Z_InstallPluginArgs struct { + A io.Reader + B bool +} + +type Z_InstallPluginReturns struct { + A *model.Manifest + B *model.AppError +} + +func (g *apiRPCClient) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) { + _args := &Z_InstallPluginArgs{file, replace} + _returns := &Z_InstallPluginReturns{} + if err := g.client.Call("Plugin.InstallPlugin", _args, _returns); err != nil { + log.Printf("RPC call to InstallPlugin API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) InstallPlugin(args *Z_InstallPluginArgs, returns *Z_InstallPluginReturns) error { + if hook, ok := s.impl.(interface { + InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) + }); ok { + returns.A, returns.B = hook.InstallPlugin(args.A, args.B) + } else { + return encodableError(fmt.Errorf("API InstallPlugin called but not implemented.")) + } + return nil +} + type Z_KVSetArgs struct { A string B []byte diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index e5f92671b2..0ce1dda159 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -5,6 +5,8 @@ package plugintest import ( + io "io" + model "github.com/mattermost/mattermost-server/model" mock "github.com/stretchr/testify/mock" ) @@ -2783,3 +2785,28 @@ func (_m *API) UploadFile(data []byte, channelId string, filename string) (*mode return r0, r1 } + +// InstallPlugin provides a mock function with given fields: file, replace +func (_m *API) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) { + ret := _m.Called(file, replace) + + var r0 *model.Manifest + if rf, ok := ret.Get(0).(func(io.Reader, bool) *model.Manifest); ok { + r0 = rf(file, replace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Manifest) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(io.Reader, bool) *model.AppError); ok { + r1 = rf(file, replace) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +}