MM-19606- Rework Prepackaged Plugins (#13449)
* MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
508fbf2f53
Коммит
87eb7697f9
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -63,11 +64,24 @@ func UseTestStore(store store.Store) {
|
||||
func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHelper {
|
||||
testStore.DropAllTables()
|
||||
|
||||
tempWorkspace, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true})
|
||||
if err != nil {
|
||||
panic("failed to initialize memory store: " + err.Error())
|
||||
}
|
||||
|
||||
config := memoryStore.Get()
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
if updateConfig != nil {
|
||||
updateConfig(config)
|
||||
}
|
||||
memoryStore.Set(config)
|
||||
|
||||
var options []app.Option
|
||||
options = append(options, app.ConfigStore(memoryStore))
|
||||
options = append(options, app.StoreOverride(testStore))
|
||||
@@ -94,9 +108,6 @@ func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHel
|
||||
})
|
||||
prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
|
||||
if updateConfig != nil {
|
||||
th.App.UpdateConfig(updateConfig)
|
||||
}
|
||||
serverErr := th.Server.Start()
|
||||
if serverErr != nil {
|
||||
panic(serverErr)
|
||||
@@ -130,11 +141,7 @@ func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHel
|
||||
th.SystemAdminClient = th.CreateClient()
|
||||
|
||||
if th.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.tempWorkspace = dir
|
||||
th.tempWorkspace = tempWorkspace
|
||||
}
|
||||
|
||||
return th
|
||||
@@ -580,9 +587,7 @@ func CheckEtag(t *testing.T, data interface{}, resp *model.Response) {
|
||||
func CheckNoError(t *testing.T, resp *model.Response) {
|
||||
t.Helper()
|
||||
|
||||
if resp.Error != nil {
|
||||
require.FailNow(t, "Expected no error, got %q", resp.Error.Error())
|
||||
}
|
||||
require.Nil(t, resp.Error, "expected no error")
|
||||
}
|
||||
|
||||
func checkHTTPStatus(t *testing.T, resp *model.Response, expectedStatus int, expectError bool) {
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
@@ -20,9 +19,6 @@ import (
|
||||
|
||||
const (
|
||||
MAXIMUM_PLUGIN_FILE_SIZE = 50 * 1024 * 1024
|
||||
// INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT defines a high timeout for installing plugins
|
||||
// from an external URL to avoid slow connections or large plugins from failing to install.
|
||||
INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT = 60 * time.Minute
|
||||
)
|
||||
|
||||
func (api *API) InitPlugin() {
|
||||
@@ -100,15 +96,15 @@ func installPluginFromUrl(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
force := r.URL.Query().Get("force") == "true"
|
||||
downloadUrl := r.URL.Query().Get("plugin_download_url")
|
||||
downloadURL := r.URL.Query().Get("plugin_download_url")
|
||||
|
||||
pluginFile, err := downloadFromUrl(c, downloadUrl)
|
||||
pluginFileBytes, err := c.App.DownloadFromURL(downloadURL)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
installPlugin(c, w, pluginFile, force)
|
||||
installPlugin(c, w, bytes.NewReader(pluginFileBytes), force)
|
||||
}
|
||||
|
||||
func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -132,28 +128,13 @@ func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_plugin_request.app_error", nil, err.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
plugin, appErr := c.App.GetMarketplacePlugin(pluginRequest)
|
||||
|
||||
manifest, appErr := c.App.InstallMarketplacePlugin(pluginRequest)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
pluginFile, appErr := downloadFromUrl(c, plugin.DownloadURL)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
signature, err := plugin.DecodeSignature()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, err.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
manifest, appErr := c.App.InstallPluginWithSignature(pluginFile, signature)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(manifest.ToJson()))
|
||||
}
|
||||
@@ -347,45 +328,20 @@ func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, e
|
||||
|
||||
filter := u.Query().Get("filter")
|
||||
serverVersion := u.Query().Get("server_version")
|
||||
localOnly, err := strconv.ParseBool(u.Query().Get("local_only"))
|
||||
if err != nil {
|
||||
localOnly = false
|
||||
}
|
||||
|
||||
return &model.MarketplacePluginFilter{
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
Filter: filter,
|
||||
ServerVersion: serverVersion,
|
||||
LocalOnly: localOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downloadFromUrl(c *Context, downloadUrl string) (io.ReadSeeker, *model.AppError) {
|
||||
if !model.IsValidHttpUrl(downloadUrl) {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(downloadUrl)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
if !*c.App.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.insecure_url.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
client := c.App.HTTPService.MakeClient(true)
|
||||
client.Timeout = INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT
|
||||
|
||||
resp, err := client.Get(downloadUrl)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
fileBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.reading_stream_failed.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return bytes.NewReader(fileBytes), nil
|
||||
}
|
||||
|
||||
func installPlugin(c *Context, w http.ResponseWriter, plugin io.ReadSeeker, force bool) {
|
||||
manifest, appErr := c.App.InstallPlugin(plugin, force)
|
||||
if appErr != nil {
|
||||
|
||||
@@ -18,8 +18,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
"github.com/mattermost/mattermost-server/v5/testlib"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -61,6 +64,10 @@ func TestPlugin(t *testing.T) {
|
||||
CheckNoError(t, resp)
|
||||
assert.Equal(t, "testplugin", manifest.Id)
|
||||
|
||||
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
require.True(t, ok)
|
||||
|
||||
t.Run("install plugin from URL with slow response time", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test to install plugin from a slow response server")
|
||||
@@ -163,7 +170,7 @@ func TestPlugin(t *testing.T) {
|
||||
assert.False(t, found)
|
||||
|
||||
// Successful activate
|
||||
ok, resp := th.SystemAdminClient.EnablePlugin(manifest.Id)
|
||||
ok, resp = th.SystemAdminClient.EnablePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
|
||||
@@ -602,7 +609,7 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
||||
DownloadURL: "",
|
||||
Labels: []model.MarketplaceLabel{{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace but was installed manually",
|
||||
Description: "This plugin is not listed in the marketplace",
|
||||
}},
|
||||
Manifest: manifest,
|
||||
},
|
||||
@@ -673,7 +680,7 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
||||
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
newPlugin.InstalledVersion = manifest.Version
|
||||
newPlugin.InstalledVersion = ""
|
||||
require.Equal(t, expectedPlugins, plugins)
|
||||
})
|
||||
}
|
||||
@@ -701,7 +708,7 @@ func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
tarDataV2, err := ioutil.ReadFile(filepath.Join(path, "testpluginv2.tar.gz"))
|
||||
tarDataV2, err := ioutil.ReadFile(filepath.Join(path, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("search installed plugin", func(t *testing.T) {
|
||||
@@ -730,37 +737,37 @@ func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||
manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData))
|
||||
CheckNoError(t, resp)
|
||||
|
||||
newPluginV1 := &model.MarketplacePlugin{
|
||||
plugin1 := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
HomepageURL: "",
|
||||
IconData: "",
|
||||
DownloadURL: "",
|
||||
Labels: []model.MarketplaceLabel{{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace but was installed manually",
|
||||
Description: "This plugin is not listed in the marketplace",
|
||||
}},
|
||||
Manifest: manifest,
|
||||
},
|
||||
InstalledVersion: manifest.Version,
|
||||
}
|
||||
expectedPlugins := append(samplePlugins, newPluginV1)
|
||||
expectedPlugins := append(samplePlugins, plugin1)
|
||||
|
||||
manifest, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarDataV2))
|
||||
CheckNoError(t, resp)
|
||||
newPluginV2 := &model.MarketplacePlugin{
|
||||
plugin2 := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
HomepageURL: "",
|
||||
IconData: "",
|
||||
DownloadURL: "",
|
||||
Labels: []model.MarketplaceLabel{{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace but was installed manually",
|
||||
Description: "This plugin is not listed in the marketplace",
|
||||
}},
|
||||
Manifest: manifest,
|
||||
},
|
||||
InstalledVersion: manifest.Version,
|
||||
}
|
||||
expectedPlugins = append(expectedPlugins, newPluginV2)
|
||||
expectedPlugins = append(expectedPlugins, plugin2)
|
||||
sort.SliceStable(expectedPlugins, func(i, j int) bool {
|
||||
return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name)
|
||||
})
|
||||
@@ -770,13 +777,13 @@ func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||
require.Equal(t, expectedPlugins, plugins)
|
||||
|
||||
// Search for plugins from the server
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "testplugin_v2"})
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "testplugin2"})
|
||||
CheckNoError(t, resp)
|
||||
require.Equal(t, []*model.MarketplacePlugin{newPluginV2}, plugins)
|
||||
require.Equal(t, []*model.MarketplacePlugin{plugin2}, plugins)
|
||||
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "dsgsdg_v2"})
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "a second plugin"})
|
||||
CheckNoError(t, resp)
|
||||
require.Equal(t, []*model.MarketplacePlugin{newPluginV2}, plugins)
|
||||
require.Equal(t, []*model.MarketplacePlugin{plugin2}, plugins)
|
||||
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "User Satisfaction Surveys"})
|
||||
CheckNoError(t, resp)
|
||||
@@ -785,6 +792,270 @@ func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "NOFILTER"})
|
||||
CheckNoError(t, resp)
|
||||
require.Nil(t, plugins)
|
||||
|
||||
// cleanup
|
||||
ok, resp := th.SystemAdminClient.RemovePlugin(plugin1.Manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
|
||||
ok, resp = th.SystemAdminClient.RemovePlugin(plugin2.Manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetLocalPluginInMarketplace(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
samplePlugins := []*model.MarketplacePlugin{
|
||||
{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
|
||||
IconData: "https://example.com/icon.svg",
|
||||
DownloadURL: "www.github.com/example",
|
||||
Manifest: &model.Manifest{
|
||||
Id: "testplugin2",
|
||||
Name: "testplugin2",
|
||||
Description: "a second plugin",
|
||||
Version: "1.2.2",
|
||||
MinServerVersion: "",
|
||||
},
|
||||
},
|
||||
InstalledVersion: "",
|
||||
},
|
||||
}
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusOK)
|
||||
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[0]})
|
||||
require.NoError(t, err)
|
||||
res.Write(json)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableMarketplace = true
|
||||
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||
})
|
||||
|
||||
t.Run("Get plugins with EnableRemoteMarketplace enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
})
|
||||
|
||||
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
require.Len(t, plugins, len(samplePlugins))
|
||||
require.Equal(t, samplePlugins, plugins)
|
||||
})
|
||||
|
||||
t.Run("get remote and local plugins", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
// Upload one local plugin
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData))
|
||||
CheckNoError(t, resp)
|
||||
|
||||
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
require.Len(t, plugins, 2)
|
||||
|
||||
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("EnableRemoteMarketplace disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
// No marketplace plugins returned
|
||||
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
require.Len(t, plugins, 0)
|
||||
|
||||
// Upload one local plugin
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData))
|
||||
CheckNoError(t, resp)
|
||||
|
||||
newPlugin := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Manifest: manifest,
|
||||
},
|
||||
InstalledVersion: manifest.Version,
|
||||
}
|
||||
|
||||
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
// Only get the local plugins
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, newPlugin, plugins[0])
|
||||
|
||||
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("local_only true", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
// Upload one local plugin
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData))
|
||||
CheckNoError(t, resp)
|
||||
|
||||
newPlugin := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Manifest: manifest,
|
||||
Labels: []model.MarketplaceLabel{{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace",
|
||||
}},
|
||||
},
|
||||
InstalledVersion: manifest.Version,
|
||||
}
|
||||
|
||||
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{LocalOnly: true})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, newPlugin, plugins[0])
|
||||
|
||||
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
marketplacePlugins := []*model.MarketplacePlugin{
|
||||
{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
|
||||
IconData: "https://example.com/icon.svg",
|
||||
DownloadURL: "www.github.com/example",
|
||||
Manifest: &model.Manifest{
|
||||
Id: "marketplace.test",
|
||||
Name: "marketplacetest",
|
||||
Description: "a marketplace plugin",
|
||||
Version: "0.1.2",
|
||||
MinServerVersion: "",
|
||||
},
|
||||
},
|
||||
InstalledVersion: "",
|
||||
},
|
||||
}
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusOK)
|
||||
json, err := json.Marshal([]*model.MarketplacePlugin{marketplacePlugins[0]})
|
||||
require.NoError(t, err)
|
||||
res.Write(json)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableMarketplace = true
|
||||
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||
})
|
||||
|
||||
prepackagePlugin := &plugin.PrepackagedPlugin{
|
||||
Manifest: &model.Manifest{
|
||||
Version: "0.0.1",
|
||||
Id: "prepackaged.test",
|
||||
},
|
||||
}
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin})
|
||||
|
||||
t.Run("get remote and prepackaged plugins", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
expectedPlugins := marketplacePlugins
|
||||
expectedPlugins = append(expectedPlugins, &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Manifest: prepackagePlugin.Manifest,
|
||||
},
|
||||
})
|
||||
|
||||
require.ElementsMatch(t, expectedPlugins, plugins)
|
||||
require.Len(t, plugins, 2)
|
||||
})
|
||||
|
||||
t.Run("EnableRemoteMarketplace disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
// No marketplace plugins returned
|
||||
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
// Only returns the prepackaged plugins
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, prepackagePlugin.Manifest, plugins[0].Manifest)
|
||||
})
|
||||
|
||||
t.Run("get prepackaged plugin if newer", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
manifest := &model.Manifest{
|
||||
Version: "1.2.3",
|
||||
Id: "marketplace.test",
|
||||
}
|
||||
|
||||
newerPrepackagePlugin := &plugin.PrepackagedPlugin{
|
||||
Manifest: manifest,
|
||||
}
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{newerPrepackagePlugin})
|
||||
|
||||
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||
CheckNoError(t, resp)
|
||||
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, newerPrepackagePlugin.Manifest, plugins[0].Manifest)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -797,15 +1068,16 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
*cfg.PluginSettings.EnableMarketplace = false
|
||||
})
|
||||
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
signatureFilename := "testpluginv2.tar.gz.sig"
|
||||
signatureFilename := "testplugin2.tar.gz.sig"
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename))
|
||||
require.Nil(t, err)
|
||||
sigFile, err := ioutil.ReadAll(signatureFileReader)
|
||||
require.Nil(t, err)
|
||||
pluginSignature := base64.StdEncoding.EncodeToString(sigFile)
|
||||
|
||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testpluginv2.tar.gz"))
|
||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
pluginServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusOK)
|
||||
@@ -820,9 +1092,9 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
IconData: "https://example.com/icon.svg",
|
||||
DownloadURL: pluginServer.URL,
|
||||
Manifest: &model.Manifest{
|
||||
Id: "testplugin_v2",
|
||||
Name: "testplugin_v2",
|
||||
Description: "dsgsdg_v2",
|
||||
Id: "testplugin2",
|
||||
Name: "testplugin2",
|
||||
Description: "a second plugin",
|
||||
Version: "1.2.2",
|
||||
MinServerVersion: "",
|
||||
},
|
||||
@@ -835,9 +1107,9 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
IconData: "https://example.com/icon.svg",
|
||||
DownloadURL: pluginServer.URL,
|
||||
Manifest: &model.Manifest{
|
||||
Id: "testplugin_v2",
|
||||
Name: "testplugin_v2",
|
||||
Description: "dsgsdg_v2",
|
||||
Id: "testplugin2",
|
||||
Name: "testplugin2",
|
||||
Description: "a second plugin",
|
||||
Version: "1.2.3",
|
||||
MinServerVersion: "",
|
||||
},
|
||||
@@ -846,7 +1118,9 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
InstalledVersion: "",
|
||||
},
|
||||
}
|
||||
|
||||
request := &model.InstallMarketplacePluginRequest{Id: "", Version: ""}
|
||||
|
||||
t.Run("marketplace disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableMarketplace = false
|
||||
@@ -856,6 +1130,7 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
CheckNotImplementedStatus(t, resp)
|
||||
require.Nil(t, plugin)
|
||||
})
|
||||
|
||||
t.Run("RequirePluginSignature enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
@@ -925,7 +1200,7 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||
*cfg.PluginSettings.AllowInsecureDownloadUrl = true
|
||||
})
|
||||
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin_v2", Version: "1.2.2"}
|
||||
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.2"}
|
||||
plugin, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckInternalErrorStatus(t, resp)
|
||||
require.Nil(t, plugin)
|
||||
@@ -945,6 +1220,7 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableMarketplace = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||
})
|
||||
|
||||
@@ -953,14 +1229,14 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
appErr := th.App.AddPublicKey("pub_key", key)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin_v2", Version: "1.2.3"}
|
||||
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"}
|
||||
manifest, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckNoError(t, resp)
|
||||
require.NotNil(t, manifest)
|
||||
require.Equal(t, "testplugin_v2", manifest.Id)
|
||||
require.Equal(t, "testplugin2", manifest.Id)
|
||||
require.Equal(t, "1.2.3", manifest.Version)
|
||||
|
||||
filePath := filepath.Join(*th.App.Config().PluginSettings.Directory, "testplugin_v2.tar.gz.sig")
|
||||
filePath := filepath.Join("plugins", "testplugin2.tar.gz.sig")
|
||||
savedSigFile, err := th.App.ReadFile(filePath)
|
||||
require.Nil(t, err)
|
||||
require.EqualValues(t, sigFile, savedSigFile)
|
||||
@@ -975,6 +1251,221 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
appErr = th.App.DeletePublicKey("pub_key")
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("install prepackaged and remote plugins through marketplace", func(t *testing.T) {
|
||||
prepackagedPluginsDir := "prepackaged_plugins"
|
||||
|
||||
os.RemoveAll(prepackagedPluginsDir)
|
||||
err := os.Mkdir(prepackagedPluginsDir, os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(prepackagedPluginsDir)
|
||||
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsDir)
|
||||
require.True(t, found, "failed to find prepackaged plugins directory")
|
||||
|
||||
err = utils.CopyFile(filepath.Join(path, "testplugin.tar.gz"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
err = utils.CopyFile(filepath.Join(path, "testplugin.tar.gz.asc"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
th := SetupConfig(func(cfg *model.Config) {
|
||||
// Disable auto-installing prepackaged plugins
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
}).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
pluginSignatureFile, err := os.Open(filepath.Join(path, "testplugin.tar.gz.asc"))
|
||||
require.Nil(t, err)
|
||||
pluginSignatureData, err := ioutil.ReadAll(pluginSignatureFile)
|
||||
require.Nil(t, err)
|
||||
|
||||
key, err := os.Open(filepath.Join(path, "development-private-key.asc"))
|
||||
require.NoError(t, err)
|
||||
appErr := th.App.AddPublicKey("pub_key", key)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
serverVersion := req.URL.Query().Get("server_version")
|
||||
require.NotEmpty(t, serverVersion)
|
||||
require.Equal(t, model.CurrentVersion, serverVersion)
|
||||
res.WriteHeader(http.StatusOK)
|
||||
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]})
|
||||
require.NoError(t, err)
|
||||
res.Write(json)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableMarketplace = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||
*cfg.PluginSettings.AllowInsecureDownloadUrl = false
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
pluginsResp, resp := th.SystemAdminClient.GetPlugins()
|
||||
CheckNoError(t, resp)
|
||||
require.Len(t, pluginsResp.Active, 0)
|
||||
require.Len(t, pluginsResp.Inactive, 0)
|
||||
|
||||
// Should fail to install unknown prepackaged plugin
|
||||
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin", Version: "0.0.2"}
|
||||
manifest, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckInternalErrorStatus(t, resp)
|
||||
require.Nil(t, manifest)
|
||||
|
||||
plugins := env.PrepackagedPlugins()
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, "testplugin", plugins[0].Manifest.Id)
|
||||
require.Equal(t, pluginSignatureData, plugins[0].Signature)
|
||||
|
||||
pluginsResp, resp = th.SystemAdminClient.GetPlugins()
|
||||
CheckNoError(t, resp)
|
||||
require.Len(t, pluginsResp.Active, 0)
|
||||
require.Len(t, pluginsResp.Inactive, 0)
|
||||
|
||||
pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin", Version: "0.0.1"}
|
||||
manifest1, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckNoError(t, resp)
|
||||
require.NotNil(t, manifest1)
|
||||
require.Equal(t, "testplugin", manifest1.Id)
|
||||
require.Equal(t, "0.0.1", manifest1.Version)
|
||||
|
||||
pluginsResp, resp = th.SystemAdminClient.GetPlugins()
|
||||
CheckNoError(t, resp)
|
||||
require.Len(t, pluginsResp.Active, 0)
|
||||
require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{
|
||||
Manifest: *manifest1,
|
||||
}})
|
||||
|
||||
// Try to install remote marketplace plugin
|
||||
pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"}
|
||||
manifest, resp = th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckInternalErrorStatus(t, resp)
|
||||
require.Nil(t, manifest)
|
||||
|
||||
// Enable remote marketplace
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableMarketplace = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||
*cfg.PluginSettings.AllowInsecureDownloadUrl = true
|
||||
})
|
||||
|
||||
pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"}
|
||||
manifest2, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckNoError(t, resp)
|
||||
require.NotNil(t, manifest2)
|
||||
require.Equal(t, "testplugin2", manifest2.Id)
|
||||
require.Equal(t, "1.2.3", manifest2.Version)
|
||||
|
||||
pluginsResp, resp = th.SystemAdminClient.GetPlugins()
|
||||
CheckNoError(t, resp)
|
||||
require.Len(t, pluginsResp.Active, 0)
|
||||
require.ElementsMatch(t, pluginsResp.Inactive, []*model.PluginInfo{
|
||||
{
|
||||
Manifest: *manifest1,
|
||||
},
|
||||
{
|
||||
Manifest: *manifest2,
|
||||
},
|
||||
})
|
||||
|
||||
// Clean up
|
||||
ok, resp := th.SystemAdminClient.RemovePlugin(manifest1.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
|
||||
ok, resp = th.SystemAdminClient.RemovePlugin(manifest2.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
|
||||
appErr = th.App.DeletePublicKey("pub_key")
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("missing prepackaged and remote plugin signatures", func(t *testing.T) {
|
||||
prepackagedPluginsDir := "prepackaged_plugins"
|
||||
|
||||
os.RemoveAll(prepackagedPluginsDir)
|
||||
err := os.Mkdir(prepackagedPluginsDir, os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(prepackagedPluginsDir)
|
||||
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsDir)
|
||||
require.True(t, found, "failed to find prepackaged plugins directory")
|
||||
|
||||
err = utils.CopyFile(filepath.Join(path, "testplugin.tar.gz"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
th := SetupConfig(func(cfg *model.Config) {
|
||||
// Disable auto-installing prepackged plugins
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
}).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
key, err := os.Open(filepath.Join(path, "development-private-key.asc"))
|
||||
require.NoError(t, err)
|
||||
appErr := th.App.AddPublicKey("pub_key", key)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
serverVersion := req.URL.Query().Get("server_version")
|
||||
require.NotEmpty(t, serverVersion)
|
||||
require.Equal(t, model.CurrentVersion, serverVersion)
|
||||
|
||||
mPlugins := []*model.MarketplacePlugin{samplePlugins[0]}
|
||||
require.Empty(t, mPlugins[0].Signature)
|
||||
res.WriteHeader(http.StatusOK)
|
||||
json, err := json.Marshal(mPlugins)
|
||||
require.NoError(t, err)
|
||||
res.Write(json)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableMarketplace = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||
*cfg.PluginSettings.AllowInsecureDownloadUrl = true
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
plugins := env.PrepackagedPlugins()
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, "testplugin", plugins[0].Manifest.Id)
|
||||
require.Empty(t, plugins[0].Signature)
|
||||
|
||||
pluginsResp, resp := th.SystemAdminClient.GetPlugins()
|
||||
CheckNoError(t, resp)
|
||||
require.Len(t, pluginsResp.Active, 0)
|
||||
require.Len(t, pluginsResp.Inactive, 0)
|
||||
|
||||
pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin", Version: "0.0.1"}
|
||||
manifest, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckInternalErrorStatus(t, resp)
|
||||
require.Nil(t, manifest)
|
||||
|
||||
pluginsResp, resp = th.SystemAdminClient.GetPlugins()
|
||||
CheckNoError(t, resp)
|
||||
require.Len(t, pluginsResp.Active, 0)
|
||||
require.Len(t, pluginsResp.Inactive, 0)
|
||||
|
||||
pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"}
|
||||
manifest, resp = th.SystemAdminClient.InstallMarketplacePlugin(pRequest)
|
||||
CheckInternalErrorStatus(t, resp)
|
||||
require.Nil(t, manifest)
|
||||
|
||||
pluginsResp, resp = th.SystemAdminClient.GetPlugins()
|
||||
CheckNoError(t, resp)
|
||||
require.Len(t, pluginsResp.Active, 0)
|
||||
require.Len(t, pluginsResp.Inactive, 0)
|
||||
|
||||
// Clean up
|
||||
appErr = th.App.DeletePublicKey("pub_key")
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
}
|
||||
|
||||
func findClusterMessages(event string, msgs []*model.ClusterMessage) []*model.ClusterMessage {
|
||||
|
||||
44
app/download.go
Обычный файл
44
app/download.go
Обычный файл
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// HTTP_REQUEST_TIMEOUT defines a high timeout for downloading large files
|
||||
// from an external URL to avoid slow connections from failing to install.
|
||||
HTTP_REQUEST_TIMEOUT = 1 * time.Hour
|
||||
)
|
||||
|
||||
func (a *App) DownloadFromURL(downloadURL string) ([]byte, error) {
|
||||
if !model.IsValidHttpUrl(downloadURL) {
|
||||
return nil, errors.Errorf("invalid url %s", downloadURL)
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(downloadURL)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to parse url %s", downloadURL)
|
||||
}
|
||||
if !*a.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" {
|
||||
return nil, errors.Errorf("insecure url not allowed %s", downloadURL)
|
||||
}
|
||||
|
||||
client := a.HTTPService.MakeClient(true)
|
||||
client.Timeout = HTTP_REQUEST_TIMEOUT
|
||||
|
||||
resp, err := client.Get(downloadURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to fetch from %s", downloadURL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
@@ -37,11 +37,21 @@ func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper {
|
||||
store := mainHelper.GetStore()
|
||||
store.DropAllTables()
|
||||
|
||||
tempWorkspace, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true})
|
||||
if err != nil {
|
||||
panic("failed to initialize memory store: " + err.Error())
|
||||
}
|
||||
|
||||
config := memoryStore.Get()
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
memoryStore.Set(config)
|
||||
|
||||
var options []Option
|
||||
options = append(options, ConfigStore(memoryStore))
|
||||
options = append(options, StoreOverride(mainHelper.Store))
|
||||
@@ -88,18 +98,9 @@ func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper {
|
||||
}
|
||||
|
||||
if th.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.tempWorkspace = dir
|
||||
th.tempWorkspace = tempWorkspace
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(th.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(th.tempWorkspace, "webapp")
|
||||
|
||||
th.App.InitPlugins(pluginDir, webappDir)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
|
||||
435
app/plugin.go
435
app/plugin.go
@@ -4,6 +4,10 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -16,9 +20,14 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v5/services/filesstore"
|
||||
"github.com/mattermost/mattermost-server/v5/services/marketplace"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
|
||||
"github.com/blang/semver"
|
||||
svg "github.com/h2non/go-is-svg"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const prepackagedPluginsDir = "prepackaged_plugins"
|
||||
|
||||
type pluginSignaturePath struct {
|
||||
pluginId string
|
||||
path string
|
||||
@@ -163,32 +172,9 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) {
|
||||
mlog.Error("Failed to sync plugins from the file store", mlog.Err(err))
|
||||
}
|
||||
|
||||
prepackagedPluginsDir, found := fileutils.FindDir("prepackaged_plugins")
|
||||
if found {
|
||||
if err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error {
|
||||
if !strings.HasSuffix(walkPath, ".tar.gz") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileReader, err := os.Open(walkPath)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to open prepackaged plugin", mlog.Err(err), mlog.String("path", walkPath))
|
||||
return nil
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
mlog.Debug("Installing prepackaged plugin", mlog.String("path", walkPath))
|
||||
|
||||
_, appErr := a.installPluginLocally(fileReader, nil, installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to unpack prepackaged plugin", mlog.Err(appErr), mlog.String("path", walkPath))
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
mlog.Error("Failed to complete unpacking prepackaged plugins", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
plugins := a.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
pluginsEnvironment = a.GetPluginsEnvironment()
|
||||
pluginsEnvironment.SetPrepackagedPlugins(plugins)
|
||||
|
||||
// Sync plugin active state when config changes. Also notify plugins.
|
||||
a.Srv.PluginsLock.Lock()
|
||||
@@ -319,7 +305,7 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
availablePlugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -327,7 +313,7 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
id = strings.ToLower(id)
|
||||
|
||||
var manifest *model.Manifest
|
||||
for _, p := range plugins {
|
||||
for _, p := range availablePlugins {
|
||||
if p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
break
|
||||
@@ -361,7 +347,7 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
availablePlugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -369,7 +355,7 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
id = strings.ToLower(id)
|
||||
|
||||
var manifest *model.Manifest
|
||||
for _, p := range plugins {
|
||||
for _, p := range availablePlugins {
|
||||
if p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
break
|
||||
@@ -423,94 +409,35 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMarketplacePlugin returns plugin from marketplace-server
|
||||
func (a *App) GetMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.BaseMarketplacePlugin, *model.AppError) {
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
filter := &model.MarketplacePluginFilter{Filter: request.Id, ServerVersion: model.CurrentVersion}
|
||||
plugin, err := marketplaceClient.GetPlugin(filter, request.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// GetMarketplacePlugins returns a list of plugins from the marketplace-server,
|
||||
// and plugins that are installed locally.
|
||||
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
|
||||
plugins := map[string]*model.MarketplacePlugin{}
|
||||
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace && !filter.LocalOnly {
|
||||
p, appErr := a.getRemotePlugins(filter)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
plugins = p
|
||||
}
|
||||
|
||||
appErr := a.mergePrepackagedPlugins(plugins)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
appErr = a.mergeLocalPlugins(plugins)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// Filter plugins.
|
||||
var result []*model.MarketplacePlugin
|
||||
pluginSet := map[string]bool{}
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Fetch all plugins from marketplace.
|
||||
marketplacePlugins, err := marketplaceClient.GetPlugins(&model.MarketplacePluginFilter{
|
||||
PerPage: -1,
|
||||
ServerVersion: model.CurrentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.marketplace_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, p := range marketplacePlugins {
|
||||
if p.Manifest == nil || !pluginMatchesFilter(p.Manifest, filter.Filter) {
|
||||
continue
|
||||
for _, p := range plugins {
|
||||
if pluginMatchesFilter(p.Manifest, filter.Filter) {
|
||||
result = append(result, p)
|
||||
}
|
||||
|
||||
marketplacePlugin := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: p,
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
if manifest, err = pluginsEnvironment.GetManifest(p.Manifest.Id); err != nil && err != plugin.ErrNotFound {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
} else if err == nil {
|
||||
// Plugin is installed.
|
||||
marketplacePlugin.InstalledVersion = manifest.Version
|
||||
}
|
||||
|
||||
pluginSet[p.Manifest.Id] = true
|
||||
result = append(result, marketplacePlugin)
|
||||
}
|
||||
|
||||
// Include all other installed plugins.
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil || pluginSet[plugin.Manifest.Id] || !pluginMatchesFilter(plugin.Manifest, filter.Filter) {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
// Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable.
|
||||
Labels: []model.MarketplaceLabel{{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace but was installed manually",
|
||||
}},
|
||||
Manifest: plugin.Manifest,
|
||||
},
|
||||
InstalledVersion: plugin.Manifest.Version,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort result alphabetically.
|
||||
@@ -521,6 +448,166 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getPrepackagedPlugin returns a pre-packaged plugin.
|
||||
func (a *App) getPrepackagedPlugin(pluginId, version string) (*plugin.PrepackagedPlugin, *model.AppError) {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.config.app_error", nil, "plugin environment is nil", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
prepackagedPlugins := pluginsEnvironment.PrepackagedPlugins()
|
||||
for _, p := range prepackagedPlugins {
|
||||
if p.Manifest.Id == pluginId && p.Manifest.Version == version {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// getRemoteMarketplacePlugin returns plugin from marketplace-server.
|
||||
func (a *App) getRemoteMarketplacePlugin(pluginId, version string) (*model.BaseMarketplacePlugin, *model.AppError) {
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
filter := &model.MarketplacePluginFilter{Filter: pluginId, ServerVersion: model.CurrentVersion}
|
||||
plugin, err := marketplaceClient.GetPlugin(filter, version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
func (a *App) getRemotePlugins(filter *model.MarketplacePluginFilter) (map[string]*model.MarketplacePlugin, *model.AppError) {
|
||||
result := map[string]*model.MarketplacePlugin{}
|
||||
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("getRemotePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Fetch all plugins from marketplace.
|
||||
marketplacePlugins, err := marketplaceClient.GetPlugins(&model.MarketplacePluginFilter{
|
||||
PerPage: -1,
|
||||
ServerVersion: model.CurrentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.failed_to_fetch", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, p := range marketplacePlugins {
|
||||
if p.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result[p.Manifest.Id] = &model.MarketplacePlugin{BaseMarketplacePlugin: p}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergePrepackagedPlugins merges pre-packaged plugins to remote marketplace plugins list.
|
||||
func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, prepackaged := range pluginsEnvironment.PrepackagedPlugins() {
|
||||
if prepackaged.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
prepackagedMarketplace := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Manifest: prepackaged.Manifest,
|
||||
},
|
||||
}
|
||||
|
||||
// If not available in marketplace, add the prepackaged
|
||||
if remoteMarketplacePlugins[prepackaged.Manifest.Id] == nil {
|
||||
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
|
||||
continue
|
||||
}
|
||||
|
||||
// If available in the markteplace, only overwrite if newer.
|
||||
prepackagedVersion, err := semver.Parse(prepackaged.Manifest.Version)
|
||||
if err != nil {
|
||||
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
marketplacePlugin := remoteMarketplacePlugins[prepackaged.Manifest.Id]
|
||||
marketplaceVersion, err := semver.Parse(marketplacePlugin.Manifest.Version)
|
||||
if err != nil {
|
||||
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if prepackagedVersion.GT(marketplaceVersion) {
|
||||
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeLocalPlugins merges locally installed plugins to remote marketplace plugins list.
|
||||
func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
localPlugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, plugin := range localPlugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if remoteMarketplacePlugins[plugin.Manifest.Id] != nil {
|
||||
// Remote plugin is installed.
|
||||
remoteMarketplacePlugins[plugin.Manifest.Id].InstalledVersion = plugin.Manifest.Version
|
||||
continue
|
||||
}
|
||||
|
||||
var labels []model.MarketplaceLabel
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace {
|
||||
// Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable.
|
||||
labels = append(labels, model.MarketplaceLabel{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace",
|
||||
})
|
||||
}
|
||||
|
||||
remoteMarketplacePlugins[plugin.Manifest.Id] = &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Labels: labels,
|
||||
Manifest: plugin.Manifest,
|
||||
},
|
||||
InstalledVersion: plugin.Manifest.Version,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginMatchesFilter(manifest *model.Manifest, filter string) bool {
|
||||
filter = strings.TrimSpace(strings.ToLower(filter))
|
||||
|
||||
@@ -600,6 +687,10 @@ func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.Ap
|
||||
return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return getPluginsFromFilePaths(fileStorePaths), nil
|
||||
}
|
||||
|
||||
func getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath {
|
||||
pluginSignaturePathMap := make(map[string]*pluginSignaturePath)
|
||||
for _, path := range fileStorePaths {
|
||||
if strings.HasSuffix(path, ".tar.gz") {
|
||||
@@ -623,5 +714,121 @@ func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.Ap
|
||||
}
|
||||
}
|
||||
|
||||
return pluginSignaturePathMap, nil
|
||||
return pluginSignaturePathMap
|
||||
}
|
||||
|
||||
func (a *App) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin {
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileStorePaths := []string{}
|
||||
err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error {
|
||||
fileStorePaths = append(fileStorePaths, walkPath)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
mlog.Error("Failed to walk prepackaged plugins", mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginSignaturePathMap := getPluginsFromFilePaths(fileStorePaths)
|
||||
plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap))
|
||||
for _, pluginPaths := range pluginSignaturePathMap {
|
||||
plugin, err := a.processPrepackagedPlugin(pluginPaths)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.String("path", pluginPaths.path), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
plugins = append(plugins, plugin)
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also
|
||||
// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true.
|
||||
func (a *App) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) {
|
||||
mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path))
|
||||
|
||||
fileReader, err := os.Open(pluginPath.path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to open prepackaged plugin %s", pluginPath.path)
|
||||
}
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to create temp dir plugintmp")
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
plugin, pluginDir, err := getPrepackagedPlugin(pluginPath, fileReader, tmpDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.path)
|
||||
}
|
||||
|
||||
// Skip installing the plugin at all if automatic prepackaged plugins is disabled
|
||||
if !*a.Config().PluginSettings.AutomaticPrepackagedPlugins {
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// Skip installing if the plugin is has not been previously enabled.
|
||||
pluginState := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]
|
||||
if pluginState == nil || !pluginState.Enable {
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path))
|
||||
if _, err := a.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// getPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
|
||||
func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
|
||||
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
|
||||
if appErr != nil {
|
||||
return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.path)
|
||||
}
|
||||
|
||||
plugin := new(plugin.PrepackagedPlugin)
|
||||
plugin.Manifest = manifest
|
||||
plugin.Path = pluginPath.path
|
||||
|
||||
if pluginPath.signaturePath != "" {
|
||||
sig := pluginPath.signaturePath
|
||||
sigReader, sigErr := os.Open(sig)
|
||||
if sigErr != nil {
|
||||
return nil, "", errors.Wrapf(sigErr, "Failed to open prepackaged plugin signature %s", sig)
|
||||
}
|
||||
bytes, sigErr := ioutil.ReadAll(sigReader)
|
||||
if sigErr != nil {
|
||||
return nil, "", errors.Wrapf(sigErr, "Failed to read prepackaged plugin signature %s", sig)
|
||||
}
|
||||
plugin.Signature = bytes
|
||||
}
|
||||
|
||||
if manifest.IconPath != "" {
|
||||
iconData, err := getIcon(manifest.IconPath)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "Failed to read icon at %s", manifest.IconPath)
|
||||
}
|
||||
plugin.IconData = iconData
|
||||
}
|
||||
|
||||
return plugin, pluginDir, nil
|
||||
}
|
||||
|
||||
func getIcon(iconPath string) (string, error) {
|
||||
icon, err := ioutil.ReadFile(iconPath)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to open icon at path %s", iconPath)
|
||||
}
|
||||
if !svg.Is(icon) {
|
||||
return "", errors.Wrapf(err, "icon is not svg %s", iconPath)
|
||||
}
|
||||
return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -45,6 +46,8 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
@@ -169,6 +172,61 @@ func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStr
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle
|
||||
// from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true.
|
||||
func (a *App) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) {
|
||||
var pluginFile, signatureFile io.ReadSeeker
|
||||
|
||||
prepackagedPlugin, appErr := a.getPrepackagedPlugin(request.Id, request.Version)
|
||||
if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" {
|
||||
return nil, appErr
|
||||
}
|
||||
if prepackagedPlugin != nil {
|
||||
fileReader, err := os.Open(prepackagedPlugin.Path)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "failed to open prepackaged plugin %s", prepackagedPlugin.Path)
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
pluginFile = fileReader
|
||||
signatureFile = bytes.NewReader(prepackagedPlugin.Signature)
|
||||
}
|
||||
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace && pluginFile == nil {
|
||||
var plugin *model.BaseMarketplacePlugin
|
||||
plugin, appErr = a.getRemoteMarketplacePlugin(request.Id, request.Version)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
downloadedPluginBytes, err := a.DownloadFromURL(plugin.DownloadURL)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
signature, err := plugin.DecodeSignature()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, err.Error(), http.StatusNotImplemented)
|
||||
}
|
||||
pluginFile = bytes.NewReader(downloadedPluginBytes)
|
||||
signatureFile = signature
|
||||
}
|
||||
|
||||
if pluginFile == nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
if signatureFile == nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
manifest, appErr := a.InstallPluginWithSignature(pluginFile, signatureFile)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
type pluginInstallationStrategy int
|
||||
|
||||
const (
|
||||
@@ -185,6 +243,7 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// verify signature
|
||||
if signature != nil {
|
||||
if err := a.VerifyPlugin(pluginFile, signature); err != nil {
|
||||
@@ -198,33 +257,55 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
pluginFile.Seek(0, 0)
|
||||
if err = utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
manifest, appErr = a.installExtractedPlugin(manifest, pluginDir, installationStrategy)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) {
|
||||
pluginFile.Seek(0, 0)
|
||||
if err := utils.ExtractTarGz(pluginFile, extractDir); err != nil {
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
dir, err := ioutil.ReadDir(extractDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
extractDir = filepath.Join(extractDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
manifest, _, err := model.FindManifest(extractDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidIdRegex}, "", http.StatusBadRequest)
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidIdRegex}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return manifest, extractDir, nil
|
||||
}
|
||||
|
||||
func (a *App) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
bundles, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Check for plugins installed with the same ID.
|
||||
@@ -239,7 +320,7 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
if existingManifest != nil {
|
||||
// Return an error if already installed and strategy disallows installation.
|
||||
if installationStrategy == installPluginLocallyOnlyIfNew {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Skip installation if already installed and newer.
|
||||
@@ -248,12 +329,12 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
|
||||
version, err = semver.Parse(manifest.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
existingVersion, err = semver.Parse(existingManifest.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if version.LTE(existingVersion) {
|
||||
@@ -265,37 +346,37 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
// Otherwise remove the existing installation prior to install below.
|
||||
mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version))
|
||||
if err := a.removePluginLocally(existingManifest.Id); err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(*a.Config().PluginSettings.Directory, manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
err = utils.CopyDir(fromPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Flag plugin locally as managed by the filestore.
|
||||
f, err := os.Create(filepath.Join(pluginPath, managedPluginFileName))
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.flag_managed.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
if manifest.HasWebapp() {
|
||||
updatedManifest, err := pluginsEnvironment.UnpackWebappBundle(manifest.Id)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.webapp_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.webapp_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
manifest = updatedManifest
|
||||
}
|
||||
|
||||
// Activate plugin if it was previously activated.
|
||||
// Activate the plugin if enabled.
|
||||
pluginState := a.Config().PluginSettings.PluginStates[manifest.Id]
|
||||
if pluginState != nil && pluginState.Enable {
|
||||
updatedManifest, _, err := pluginsEnvironment.Activate(manifest.Id)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.restart.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
manifest = updatedManifest
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
)
|
||||
|
||||
@@ -485,108 +486,316 @@ func TestPluginSync(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
os.MkdirAll("./test-plugins", os.ModePerm)
|
||||
defer os.RemoveAll("./test-plugins")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.Directory = "./test-plugins"
|
||||
*cfg.PluginSettings.ClientDirectory = "./test-client-plugins"
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
testCase.ConfigFunc(cfg)
|
||||
})
|
||||
th.App.UpdateConfig(testCase.ConfigFunc)
|
||||
|
||||
env, err := plugin.NewEnvironment(th.App.NewPluginAPI, "./test-plugins", "./test-client-plugins", th.App.Log)
|
||||
require.NoError(t, err)
|
||||
th.App.SetPluginsEnvironment(env)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
require.NotNil(t, env)
|
||||
|
||||
// New bundle in the file store case
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer fileReader.Close()
|
||||
|
||||
_, appErr := th.App.WriteFile(fileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
t.Run("new bundle in the file store", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
})
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer fileReader.Close()
|
||||
|
||||
// Check if installed
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
_, appErr := th.App.WriteFile(fileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
// Bundle removed from the file store case
|
||||
appErr = th.App.RemoveFile(th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
// Check if removed
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
|
||||
// RequirePluginSignature = true case
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
// Check if installed
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
})
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
_, appErr = th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
// no signature
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
|
||||
// Wrong signature
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, "testpluginv2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
filePath := fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
|
||||
checkNoError(t, appErr)
|
||||
t.Run("bundle removed from the file store", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
})
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
appErr := th.App.RemoveFile(th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
// Correct signature
|
||||
key, err := os.Open(filepath.Join(path, "development-private-key.asc"))
|
||||
require.NoError(t, err)
|
||||
appErr = th.App.AddPublicKey("pub_key", key)
|
||||
checkNoError(t, appErr)
|
||||
// Check if removed
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
})
|
||||
|
||||
signatureFileReader, err = os.Open(filepath.Join(path, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
filePath = fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
|
||||
checkNoError(t, appErr)
|
||||
t.Run("plugin signatures required, no signature", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
})
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
_, appErr := th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
appErr = th.App.DeletePublicKey("pub_key")
|
||||
checkNoError(t, appErr)
|
||||
t.Run("plugin signatures required, wrong signature", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
})
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
_, appErr := th.App.WriteFile(signatureFileReader, th.App.getSignatureStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("plugin signatures required, correct signature", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
})
|
||||
|
||||
key, err := os.Open(filepath.Join(path, "development-private-key.asc"))
|
||||
require.NoError(t, err)
|
||||
appErr := th.App.AddPublicKey("pub_key", key)
|
||||
checkNoError(t, appErr)
|
||||
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, th.App.getSignatureStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
|
||||
appErr = th.App.DeletePublicKey("pub_key")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPrepackagedPlugins(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
testsPath, _ := fileutils.FindDir("tests")
|
||||
prepackagedPluginsPath := filepath.Join(testsPath, prepackagedPluginsDir)
|
||||
fileErr := os.Mkdir(prepackagedPluginsPath, os.ModePerm)
|
||||
require.NoError(t, fileErr)
|
||||
defer os.RemoveAll(prepackagedPluginsPath)
|
||||
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsPath)
|
||||
require.True(t, found, "failed to find prepackaged plugins directory")
|
||||
|
||||
testPluginPath := filepath.Join(testsPath, "testplugin.tar.gz")
|
||||
fileErr = utils.CopyFile(testPluginPath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz"))
|
||||
require.NoError(t, fileErr)
|
||||
|
||||
t.Run("automatic, enabled plugin, no signature", func(t *testing.T) {
|
||||
// Install the plugin and enable
|
||||
pluginBytes, err := ioutil.ReadFile(testPluginPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pluginBytes)
|
||||
|
||||
manifest, appErr := th.App.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin", manifest.Id)
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
activatedManifest, activated, err := env.Activate(manifest.Id)
|
||||
require.NoError(t, err)
|
||||
require.True(t, activated)
|
||||
require.Equal(t, manifest, activatedManifest)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, plugins[0].Manifest.Id, "testplugin")
|
||||
require.Empty(t, plugins[0].Signature, 0)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("automatic, not enabled plugin", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, plugins[0].Manifest.Id, "testplugin")
|
||||
require.Empty(t, plugins[0].Signature, 0)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("automatic, multiple plugins with signatures, not enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
// Add signature
|
||||
testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig")
|
||||
err := utils.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add second plugin
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err = utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("automatic, multiple plugins with signatures, one enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
// Add signature
|
||||
testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig")
|
||||
err := utils.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Install first plugin and enable
|
||||
pluginBytes, err := ioutil.ReadFile(testPluginPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pluginBytes)
|
||||
|
||||
manifest, appErr := th.App.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin", manifest.Id)
|
||||
|
||||
activatedManifest, activated, err := env.Activate(manifest.Id)
|
||||
require.NoError(t, err)
|
||||
require.True(t, activated)
|
||||
require.Equal(t, manifest, activatedManifest)
|
||||
|
||||
// Add second plugin
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err = utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("non-automatic, multiple plugins", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err := utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ endif
|
||||
mkdir -p tmpprepackaged
|
||||
@cd tmpprepackaged && for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
curl -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz; \
|
||||
curl -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz.sig; \
|
||||
done
|
||||
|
||||
@# ----- PLATFORM SPECIFIC -----
|
||||
@@ -83,6 +84,7 @@ endif
|
||||
@# Strip and prepackage plugins
|
||||
@for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
cat tmpprepackaged/$$plugin_package.tar.gz | gunzip | tar --wildcards --delete "*windows*" --delete "*linux*" | gzip > $(DIST_PATH)/prepackaged_plugins/$$plugin_package.tar.gz; \
|
||||
cp tmpprepackaged/$$plugin_package.tar.gz.sig $(DIST_PATH)/prepackaged_plugins; \
|
||||
done
|
||||
@# Package
|
||||
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-osx-amd64.tar.gz mattermost
|
||||
@@ -103,6 +105,7 @@ endif
|
||||
@# Strip and prepackage plugins
|
||||
@for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
cat tmpprepackaged/$$plugin_package.tar.gz | gunzip | tar --wildcards --delete "*darwin*" --delete "*linux*" | gzip > $(DIST_PATH)/prepackaged_plugins/$$plugin_package.tar.gz; \
|
||||
cp tmpprepackaged/$$plugin_package.tar.gz.sig $(DIST_PATH)/prepackaged_plugins; \
|
||||
done
|
||||
@# Package
|
||||
cd $(DIST_ROOT) && zip -9 -r -q -l mattermost-$(BUILD_TYPE_NAME)-windows-amd64.zip mattermost && cd ..
|
||||
@@ -123,6 +126,7 @@ endif
|
||||
@# Strip and prepackage plugins
|
||||
@for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
cat tmpprepackaged/$$plugin_package.tar.gz | gunzip | tar --wildcards --delete "*windows*" --delete "*darwin*" | gzip > $(DIST_PATH)/prepackaged_plugins/$$plugin_package.tar.gz; \
|
||||
cp tmpprepackaged/$$plugin_package.tar.gz.sig $(DIST_PATH)/prepackaged_plugins; \
|
||||
done
|
||||
@# Package
|
||||
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-linux-amd64.tar.gz mattermost
|
||||
|
||||
1
go.mod
1
go.mod
@@ -26,6 +26,7 @@ require (
|
||||
github.com/gorilla/schema v1.1.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
|
||||
github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84
|
||||
github.com/hashicorp/go-hclog v0.9.2
|
||||
github.com/hashicorp/go-immutable-radix v1.1.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -155,6 +155,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:Fecb
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc=
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE=
|
||||
github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84 h1:RvcDqcKLua4b/jtXez7ZVe9s6Iq5N6ujVevqY4FBQmM=
|
||||
github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
|
||||
30
i18n/en.json
30
i18n/en.json
@@ -1572,18 +1572,6 @@
|
||||
"id": "api.plugin.install.download_failed.app_error",
|
||||
"translation": "An error occurred while downloading the plugin."
|
||||
},
|
||||
{
|
||||
"id": "api.plugin.install.insecure_url.app_error",
|
||||
"translation": "An insecure url was given to download the plugin. Please provide a secure url or enable PluginSettings.AllowInsecureDownloadUrl in your configuration."
|
||||
},
|
||||
{
|
||||
"id": "api.plugin.install.invalid_url.app_error",
|
||||
"translation": "An invalid url was given to download the plugin."
|
||||
},
|
||||
{
|
||||
"id": "api.plugin.install.reading_stream_failed.app_error",
|
||||
"translation": "An error ocurred reading the plugin file stream."
|
||||
},
|
||||
{
|
||||
"id": "api.plugin.upload.array.app_error",
|
||||
"translation": "File array is empty in multipart/form request"
|
||||
@@ -3584,7 +3572,7 @@
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.extract.app_error",
|
||||
"translation": "Encountered error extracting plugin"
|
||||
"translation": "An error occurred extracting the plugin bundle."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.filesystem.app_error",
|
||||
@@ -3622,6 +3610,10 @@
|
||||
"id": "app.plugin.install_id_failed_remove.app_error",
|
||||
"translation": "Unable to install plugin. A plugin with the same ID is already installed and failed to be removed."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.install_marketplace_plugin.app_error",
|
||||
"translation": "Failed to install marketplace plugin."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.invalid_id.app_error",
|
||||
"translation": "Plugin Id must be at least {{.Min}} characters, at most {{.Max}} characters and match {{.Regex}}."
|
||||
@@ -3638,6 +3630,10 @@
|
||||
"id": "app.plugin.marketplace_client.app_error",
|
||||
"translation": "Failed to create marketplace client."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.marketplace_client.failed_to_fetch",
|
||||
"translation": "Failed to get plugins from the marketplace server."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.marketplace_disabled.app_error",
|
||||
"translation": "Marketplace has been disabled. Please check your logs for details."
|
||||
@@ -3646,14 +3642,14 @@
|
||||
"id": "app.plugin.marketplace_plugin_request.app_error",
|
||||
"translation": "Failed to decode the marketplace plugin request."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.marketplace_plugins.app_error",
|
||||
"translation": "Failed to get plugins from the marketplace server."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.marketplace_plugins.not_found.app_error",
|
||||
"translation": "Could not find the requested marketplace plugin."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.marketplace_plugins.signature_not_found.app_error",
|
||||
"translation": "Could not find the requested marketplace plugin signature."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.marshal.app_error",
|
||||
"translation": "Failed to marshal marketplace plugins."
|
||||
|
||||
@@ -2265,18 +2265,20 @@ type PluginState struct {
|
||||
}
|
||||
|
||||
type PluginSettings struct {
|
||||
Enable *bool
|
||||
EnableUploads *bool `restricted:"true"`
|
||||
AllowInsecureDownloadUrl *bool `restricted:"true"`
|
||||
EnableHealthCheck *bool `restricted:"true"`
|
||||
Directory *string `restricted:"true"`
|
||||
ClientDirectory *string `restricted:"true"`
|
||||
Plugins map[string]map[string]interface{}
|
||||
PluginStates map[string]*PluginState
|
||||
EnableMarketplace *bool
|
||||
RequirePluginSignature *bool
|
||||
MarketplaceUrl *string
|
||||
SignaturePublicKeyFiles []string
|
||||
Enable *bool
|
||||
EnableUploads *bool `restricted:"true"`
|
||||
AllowInsecureDownloadUrl *bool `restricted:"true"`
|
||||
EnableHealthCheck *bool `restricted:"true"`
|
||||
Directory *string `restricted:"true"`
|
||||
ClientDirectory *string `restricted:"true"`
|
||||
Plugins map[string]map[string]interface{}
|
||||
PluginStates map[string]*PluginState
|
||||
EnableMarketplace *bool
|
||||
EnableRemoteMarketplace *bool
|
||||
AutomaticPrepackagedPlugins *bool
|
||||
RequirePluginSignature *bool
|
||||
MarketplaceUrl *string
|
||||
SignaturePublicKeyFiles []string
|
||||
}
|
||||
|
||||
func (s *PluginSettings) SetDefaults(ls LogSettings) {
|
||||
@@ -2321,6 +2323,14 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) {
|
||||
s.EnableMarketplace = NewBool(PLUGIN_SETTINGS_DEFAULT_ENABLE_MARKETPLACE)
|
||||
}
|
||||
|
||||
if s.EnableRemoteMarketplace == nil {
|
||||
s.EnableRemoteMarketplace = NewBool(true)
|
||||
}
|
||||
|
||||
if s.AutomaticPrepackagedPlugins == nil {
|
||||
s.AutomaticPrepackagedPlugins = NewBool(true)
|
||||
}
|
||||
|
||||
if s.MarketplaceUrl == nil || *s.MarketplaceUrl == "" || *s.MarketplaceUrl == PLUGIN_SETTINGS_OLD_MARKETPLACE_URL {
|
||||
s.MarketplaceUrl = NewString(PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ type MarketplacePluginFilter struct {
|
||||
PerPage int
|
||||
Filter string
|
||||
ServerVersion string
|
||||
LocalOnly bool
|
||||
}
|
||||
|
||||
// ApplyToURL modifies the given url to include query string parameters for the request.
|
||||
@@ -89,6 +90,7 @@ func (filter *MarketplacePluginFilter) ApplyToURL(u *url.URL) {
|
||||
}
|
||||
q.Add("filter", filter.Filter)
|
||||
q.Add("server_version", filter.ServerVersion)
|
||||
q.Add("local_only", strconv.FormatBool(filter.LocalOnly))
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
|
||||
@@ -36,17 +36,27 @@ type registeredPlugin struct {
|
||||
supervisor *supervisor
|
||||
}
|
||||
|
||||
// PrepackagedPlugin is a plugin prepackaged with the server and found on startup.
|
||||
type PrepackagedPlugin struct {
|
||||
Path string
|
||||
IconData string
|
||||
Manifest *model.Manifest
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// Environment represents the execution environment of active plugins.
|
||||
//
|
||||
// It is meant for use by the Mattermost server to manipulate, interact with and report on the set
|
||||
// of active plugins.
|
||||
type Environment struct {
|
||||
registeredPlugins sync.Map
|
||||
pluginHealthCheckJob *PluginHealthCheckJob
|
||||
logger *mlog.Logger
|
||||
newAPIImpl apiImplCreatorFunc
|
||||
pluginDir string
|
||||
webappPluginDir string
|
||||
registeredPlugins sync.Map
|
||||
pluginHealthCheckJob *PluginHealthCheckJob
|
||||
logger *mlog.Logger
|
||||
newAPIImpl apiImplCreatorFunc
|
||||
pluginDir string
|
||||
webappPluginDir string
|
||||
prepackagedPlugins []*PrepackagedPlugin
|
||||
prepackagedPluginsLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewEnvironment(newAPIImpl apiImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) {
|
||||
@@ -87,6 +97,15 @@ func (env *Environment) Available() ([]*model.BundleInfo, error) {
|
||||
return scanSearchPath(env.pluginDir)
|
||||
}
|
||||
|
||||
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder.
|
||||
// The list content is immutable and should not be modified.
|
||||
func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin {
|
||||
env.prepackagedPluginsLock.RLock()
|
||||
defer env.prepackagedPluginsLock.RUnlock()
|
||||
|
||||
return env.prepackagedPlugins
|
||||
}
|
||||
|
||||
// Returns a list of all currently active plugins within the environment.
|
||||
func (env *Environment) Active() []*model.BundleInfo {
|
||||
activePlugins := []*model.BundleInfo{}
|
||||
@@ -439,6 +458,13 @@ func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool
|
||||
})
|
||||
}
|
||||
|
||||
// SetPrepackagedPlugins saves prepackaged plugins in the environment.
|
||||
func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) {
|
||||
env.prepackagedPluginsLock.Lock()
|
||||
env.prepackagedPlugins = plugins
|
||||
env.prepackagedPluginsLock.Unlock()
|
||||
}
|
||||
|
||||
func newRegisteredPlugin(bundle *model.BundleInfo) *registeredPlugin {
|
||||
state := model.PluginStateNotRunning
|
||||
return ®isteredPlugin{failTimeStamps: []time.Time{}, State: state, BundleInfo: bundle}
|
||||
|
||||
Двоичные данные
tests/testplugin.tar.gz
Двоичные данные
tests/testplugin.tar.gz
Двоичный файл не отображается.
@@ -1,14 +1,15 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
-----BEGIN PGP ARMORED FILE-----
|
||||
Comment: Use "gpg --dearmor" for unpacking
|
||||
|
||||
iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAl26+QIACgkQTHxlYsGS
|
||||
zB9SFwwAmexJelfpTRjnABJKWnFKuGOTNosVNOWqE+k//n1PV3H3uCzsluh/GWIb
|
||||
p5Nk6+UJieNvyAaQhIT5+3Rv0HkYvz4N15813SvQ8KOL8vE7oRFHttsZHpDKTpk1
|
||||
fQrd9JISMdViVj4x8VltgYkUIJDmULPfF9/m1OGV/BsWuHmmaEp3yw+KLdhL1j3U
|
||||
LlfW453nHvXrGT2rO3l+KDIVMt4zG6LVmL1C29yWtBplKxjgXKg/EbPcI31/8D9X
|
||||
QoIz9fUIFzqt4L1IfJhL6NlUiDc1iSKf4STiU2CzmHdKqRD9FdwgzZlG5P/xxCgu
|
||||
BVNy/c9musZbK9LtapncC/S62uwZzH5naTrCDs4F1VE2P5gjU9iv3AXVaeT4hLuI
|
||||
EbluXzmAt9hjox2s0O0X6EgW68FbUVX9bvu1tY9sUvWlXNCL2hrybF3YS3cZ2wGx
|
||||
rmdWmwktXDpJ8iN/b1+quw0CVC7myAefPK0U8356tktZHwcQEp2fmrF4PqX2qfJf
|
||||
cDzhj2yg
|
||||
=J9kV
|
||||
-----END PGP SIGNATURE-----
|
||||
iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAl3vHFQACgkQTHxlYsGS
|
||||
zB933gv5AcDL/MW5/fwsLnhWW9plSCbRTMOckt94r0ASOKRzPP7ngEzGtkCFEIGQ
|
||||
bN/T60s9uADuIRHEFhmtZvG2MOg9Z688BxiDpyyTx7ENZH0zzPnULEg5hqf7DJqK
|
||||
Q8IdPSNE4wk4gvJLdrYIYUkQT4TkQonmBbIHvj3wWKaXTheDnUNp90pOiTszKubK
|
||||
ghpAj6ZAlRK60HuMMF7v0RIhqLKYKUGUWvzClKEUZlAKlqTK++FTM0Zot/7KTLDz
|
||||
KEv8gf6xe4oL7vSQBoZDa3Vvcn6tPF0gvwg2DrITT3MG2Rmgpbc74iUXWqoZ+XQp
|
||||
zf1P+vxbfbyKMYZ4tERlKfwnl6Dbz5vBvlz/U+ZYBmwuw4wD2DvdrecPHj1KcBNy
|
||||
2Auap8qhMQcZvYV7D/qnYL8/QU4roQdg4z+G/S0vQTlK5izpzY+/M8ofZHtjDdWD
|
||||
cD3w1cvwBFKn9EBJ1HA4/7FEHUzFrh/kWRw2PpUjkydCur2rHqecpRXBqS+Haz7J
|
||||
OT9k54ab
|
||||
=ywEK
|
||||
-----END PGP ARMORED FILE-----
|
||||
|
||||
Двоичные данные
tests/testplugin.tar.gz.sig
Двоичные данные
tests/testplugin.tar.gz.sig
Двоичный файл не отображается.
Двоичные данные
tests/testplugin2.tar.gz
Обычный файл
Двоичные данные
tests/testplugin2.tar.gz
Обычный файл
Двоичный файл не отображается.
Двоичные данные
tests/testplugin2.tar.gz.sig
Обычный файл
Двоичные данные
tests/testplugin2.tar.gz.sig
Обычный файл
Двоичный файл не отображается.
Двоичные данные
tests/testpluginv2.tar.gz
Двоичные данные
tests/testpluginv2.tar.gz
Двоичный файл не отображается.
Двоичные данные
tests/testpluginv2.tar.gz.sig
Двоичные данные
tests/testpluginv2.tar.gz.sig
Двоичный файл не отображается.
12
vendor/github.com/h2non/go-is-svg/.editorconfig
сгенерированный
поставляемый
Обычный файл
12
vendor/github.com/h2non/go-is-svg/.editorconfig
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tabs
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
7
vendor/github.com/h2non/go-is-svg/.gitignore
сгенерированный
поставляемый
Обычный файл
7
vendor/github.com/h2non/go-is-svg/.gitignore
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,7 @@
|
||||
/bimg
|
||||
/bundle
|
||||
bin
|
||||
/*.jpg
|
||||
/*.png
|
||||
/*.webp
|
||||
/fixtures/*_out.*
|
||||
23
vendor/github.com/h2non/go-is-svg/.travis.yml
сгенерированный
поставляемый
Обычный файл
23
vendor/github.com/h2non/go-is-svg/.travis.yml
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,23 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- go get github.com/nbio/st
|
||||
- go get -u -v github.com/axw/gocov/gocov
|
||||
- go get -u -v github.com/mattn/goveralls
|
||||
- go get -u -v github.com/golang/lint/golint
|
||||
|
||||
script:
|
||||
- diff -u <(echo -n) <(gofmt -s -d ./)
|
||||
- diff -u <(echo -n) <(go vet ./...)
|
||||
- diff -u <(echo -n) <(golint ./...)
|
||||
- go test -v -race ./...
|
||||
- go test -v -race -covermode=atomic -coverprofile=coverage.out
|
||||
|
||||
after_success:
|
||||
- goveralls -coverprofile=coverage.out -service=travis-ci
|
||||
24
vendor/github.com/h2non/go-is-svg/LICENSE
сгенерированный
поставляемый
Обычный файл
24
vendor/github.com/h2non/go-is-svg/LICENSE
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,24 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2016 Tomas Aparicio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
47
vendor/github.com/h2non/go-is-svg/README.md
сгенерированный
поставляемый
Обычный файл
47
vendor/github.com/h2non/go-is-svg/README.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,47 @@
|
||||
# go-is-svg [](https://travis-ci.org/h2non/go-is-svg) [](https://godoc.org/github.com/h2non/go-is-svg) [](https://coveralls.io/github/h2non/go-is-svg?branch=master) [](https://goreportcard.com/report/github.com/h2non/go-is-svg)
|
||||
|
||||
Tiny package to verify if a given file buffer is an SVG image in Go (golang).
|
||||
|
||||
See also [filetype](https://github.com/h2non/filetype) package for binary files type inference.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get -u github.com/h2non/go-is-svg
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
svg "github.com/h2non/go-is-svg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
buf, err := ioutil.ReadFile("_example/example.svg")
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if svg.Is(buf) {
|
||||
fmt.Println("File is an SVG")
|
||||
} else {
|
||||
fmt.Println("File is NOT an SVG")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run example:
|
||||
```bash
|
||||
go run _example/example.go
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT - Tomas Aparicio
|
||||
36
vendor/github.com/h2non/go-is-svg/svg.go
сгенерированный
поставляемый
Обычный файл
36
vendor/github.com/h2non/go-is-svg/svg.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,36 @@
|
||||
package issvg
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
htmlCommentRegex = regexp.MustCompile("(?i)<!--([\\s\\S]*?)-->")
|
||||
svgRegex = regexp.MustCompile(`(?i)^\s*(?:<\?xml[^>]*>\s*)?(?:<!doctype svg[^>]*>\s*)?<svg[^>]*>[^*]*<\/svg>\s*$`)
|
||||
)
|
||||
|
||||
// isBinary checks if the given buffer is a binary file.
|
||||
func isBinary(buf []byte) bool {
|
||||
if len(buf) < 24 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < 24; i++ {
|
||||
charCode, _ := utf8.DecodeRuneInString(string(buf[i]))
|
||||
if charCode == 65533 || charCode <= 8 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Is returns true if the given buffer is a valid SVG image.
|
||||
func Is(buf []byte) bool {
|
||||
return !isBinary(buf) && svgRegex.Match(htmlCommentRegex.ReplaceAll(buf, []byte{}))
|
||||
}
|
||||
|
||||
// IsSVG returns true if the given buffer is a valid SVG image.
|
||||
// Alias to: Is()
|
||||
func IsSVG(buf []byte) bool {
|
||||
return Is(buf)
|
||||
}
|
||||
2
vendor/modules.txt
поставляемый
2
vendor/modules.txt
поставляемый
@@ -52,6 +52,8 @@ github.com/gorilla/schema
|
||||
github.com/gorilla/websocket
|
||||
# github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
||||
github.com/gregjones/httpcache
|
||||
# github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
|
||||
github.com/h2non/go-is-svg
|
||||
# github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84
|
||||
github.com/hako/durafmt
|
||||
# github.com/hashicorp/errwrap v1.0.0
|
||||
|
||||
Ссылка в новой задаче
Block a user