MM-17023: Plugin Marketplace (#12183)
* MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * MM-17150, MM-17545, MM-18100 - Implement GET /api/v4/plugins/m… (#11977) * MM-17150 - Implement GET /api/v4/plugins/marketplace proxying upstream MM-17545 - Merge locally installed plugins into GET /api/v4/plugins/marketplace * Replaced MarketplacePluginState with Installed * Setting InstalledVersion instead of Installed * marketplace client setting per_page if non zero * Creating insecure client for marketplace url * Fixed trailing slash for default marketplace url * Adding filtering * Fixed function names * Renamed Manifest() to GetManifest(), added godoc for BaseMarketplacePlugin * Handling plugin.ErrNotFound correctly * Checking err == nil instead when a plugin is installed * MM-18450 - Local-only plugin search (#12152) * MM-17846: plugin icons (#12157) * MM-17846: add support for plugin icons Extend the model definitions to support plugin icons from the marketplace. * s/IconURL/IconData * MM-18475 - Converge on snake_case responses from the marketplace (#12179) * MM-18520 - MM-Server should forward server version to marketplace server (#12181) * Renamed request to filter client4.GetMarketplacePlugins * Renamed request to filter * Guarding against bad marketplace server response
Этот коммит содержится в:
коммит произвёл
Ali Farooq
родитель
86891091c0
Коммит
4ce7b92283
25
api4/helpers.go
Обычный файл
25
api4/helpers.go
Обычный файл
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package api4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseInt(u *url.URL, name string, defaultValue int) (int, error) {
|
||||||
|
valueStr := u.Query().Get(name)
|
||||||
|
if valueStr == "" {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := strconv.Atoi(valueStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrapf(err, "failed to parse %s as integer", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ package api4
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -36,6 +37,8 @@ func (api *API) InitPlugin() {
|
|||||||
api.BaseRoutes.Plugin.Handle("/disable", api.ApiSessionRequired(disablePlugin)).Methods("POST")
|
api.BaseRoutes.Plugin.Handle("/disable", api.ApiSessionRequired(disablePlugin)).Methods("POST")
|
||||||
|
|
||||||
api.BaseRoutes.Plugins.Handle("/webapp", api.ApiHandler(getWebappPlugins)).Methods("GET")
|
api.BaseRoutes.Plugins.Handle("/webapp", api.ApiHandler(getWebappPlugins)).Methods("GET")
|
||||||
|
|
||||||
|
api.BaseRoutes.Plugins.Handle("/marketplace", api.ApiSessionRequired(getMarketplacePlugins)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -239,6 +242,43 @@ func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(model.ManifestListToJson(clientManifests)))
|
w.Write([]byte(model.ManifestListToJson(clientManifests)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMarketplacePlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !*c.App.Config().PluginSettings.Enable {
|
||||||
|
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*c.App.Config().PluginSettings.EnableMarketplace {
|
||||||
|
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marketplace_disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := parseMarketplacePluginFilter(r.URL)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins, appErr := c.App.GetMarketplacePlugins(filter)
|
||||||
|
if appErr != nil {
|
||||||
|
c.Err = appErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := json.Marshal(plugins)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("getMarketplacePlugins", "app.plugin.marshal.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(json)
|
||||||
|
}
|
||||||
|
|
||||||
func enablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
func enablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
c.RequirePluginId()
|
c.RequirePluginId()
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
@@ -286,3 +326,25 @@ func disablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ReturnStatusOK(w)
|
ReturnStatusOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, error) {
|
||||||
|
page, err := parseInt(u, "page", 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
perPage, err := parseInt(u, "per_page", 100)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := u.Query().Get("filter")
|
||||||
|
serverVersion := u.Query().Get("server_version")
|
||||||
|
|
||||||
|
return &model.MarketplacePluginFilter{
|
||||||
|
Page: page,
|
||||||
|
PerPage: perPage,
|
||||||
|
Filter: filter,
|
||||||
|
ServerVersion: serverVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -36,9 +38,7 @@ func TestPlugin(t *testing.T) {
|
|||||||
|
|
||||||
path, _ := fileutils.FindDir("tests")
|
path, _ := fileutils.FindDir("tests")
|
||||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install from URL
|
// Install from URL
|
||||||
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
@@ -270,9 +270,7 @@ func TestNotifyClusterPluginEvent(t *testing.T) {
|
|||||||
|
|
||||||
path, _ := fileutils.FindDir("tests")
|
path, _ := fileutils.FindDir("tests")
|
||||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCluster.ClearMessages()
|
testCluster.ClearMessages()
|
||||||
|
|
||||||
@@ -331,9 +329,7 @@ func TestNotifyClusterPluginEvent(t *testing.T) {
|
|||||||
func TestDisableOnRemove(t *testing.T) {
|
func TestDisableOnRemove(t *testing.T) {
|
||||||
path, _ := fileutils.FindDir("tests")
|
path, _ := fileutils.FindDir("tests")
|
||||||
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Description string
|
Description string
|
||||||
@@ -432,6 +428,324 @@ func TestDisableOnRemove(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetMarketplacePlugins(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.Enable = true
|
||||||
|
*cfg.PluginSettings.EnableUploads = true
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = false
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("marketplace disabled", func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = false
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = "invalid.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNotImplementedStatus(t, resp)
|
||||||
|
require.Nil(t, plugins)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no server", func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = true
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = "invalid.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckInternalErrorStatus(t, resp)
|
||||||
|
require.Nil(t, plugins)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no permission", func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = true
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = "invalid.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.Client.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
require.Nil(t, plugins)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty response from server", func(t *testing.T) {
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
res.WriteHeader(http.StatusOK)
|
||||||
|
json, err := json.Marshal([]*model.MarketplacePlugin{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
res.Write(json)
|
||||||
|
}))
|
||||||
|
defer func() { testServer.Close() }()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = true
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Len(t, plugins, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verify server version is passed through", func(t *testing.T) {
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
serverVersion, ok := req.URL.Query()["server_version"]
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, serverVersion, 1)
|
||||||
|
require.Equal(t, model.CurrentVersion, serverVersion[0])
|
||||||
|
require.NotEqual(t, 0, len(serverVersion[0]))
|
||||||
|
|
||||||
|
res.WriteHeader(http.StatusOK)
|
||||||
|
json, err := json.Marshal([]*model.MarketplacePlugin{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
res.Write(json)
|
||||||
|
}))
|
||||||
|
defer func() { testServer.Close() }()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = true
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Len(t, plugins, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
||||||
|
samplePlugins := []*model.MarketplacePlugin{
|
||||||
|
{
|
||||||
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
|
HomepageURL: "https://github.com/mattermost/mattermost-plugin-nps",
|
||||||
|
IconData: "http://example.com/icon.svg",
|
||||||
|
DownloadURL: "https://github.com/mattermost/mattermost-plugin-nps/releases/download/v1.0.3/com.mattermost.nps-1.0.3.tar.gz",
|
||||||
|
Manifest: &model.Manifest{
|
||||||
|
Id: "com.mattermost.nps",
|
||||||
|
Name: "User Satisfaction Surveys",
|
||||||
|
Description: "This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost.",
|
||||||
|
Version: "1.0.3",
|
||||||
|
MinServerVersion: "5.14.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InstalledVersion: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
path, _ := fileutils.FindDir("tests")
|
||||||
|
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("marketplace client returns not-installed plugin", func(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
res.WriteHeader(http.StatusOK)
|
||||||
|
json, err := json.Marshal(samplePlugins)
|
||||||
|
require.NoError(t, err)
|
||||||
|
res.Write(json)
|
||||||
|
}))
|
||||||
|
defer func() { testServer.Close() }()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.Enable = true
|
||||||
|
*cfg.PluginSettings.EnableUploads = true
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = true
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, samplePlugins, plugins)
|
||||||
|
|
||||||
|
manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData))
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
expectedPlugins := append(samplePlugins, &model.MarketplacePlugin{
|
||||||
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
|
HomepageURL: "",
|
||||||
|
IconData: "",
|
||||||
|
DownloadURL: "",
|
||||||
|
Manifest: manifest,
|
||||||
|
},
|
||||||
|
InstalledVersion: manifest.Version,
|
||||||
|
})
|
||||||
|
sort.SliceStable(expectedPlugins, func(i, j int) bool {
|
||||||
|
return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, expectedPlugins, plugins)
|
||||||
|
|
||||||
|
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, samplePlugins, plugins)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("marketplace client returns installed plugin", func(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.Enable = true
|
||||||
|
*cfg.PluginSettings.EnableUploads = true
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = true
|
||||||
|
})
|
||||||
|
|
||||||
|
manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData))
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
newPlugin := &model.MarketplacePlugin{
|
||||||
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
|
HomepageURL: "HomepageURL",
|
||||||
|
IconData: "IconData",
|
||||||
|
DownloadURL: "DownloadURL",
|
||||||
|
Manifest: manifest,
|
||||||
|
},
|
||||||
|
InstalledVersion: manifest.Version,
|
||||||
|
}
|
||||||
|
expectedPlugins := append(samplePlugins, newPlugin)
|
||||||
|
sort.SliceStable(expectedPlugins, func(i, j int) bool {
|
||||||
|
return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
res.WriteHeader(http.StatusOK)
|
||||||
|
json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[0], newPlugin})
|
||||||
|
require.NoError(t, err)
|
||||||
|
res.Write(json)
|
||||||
|
}))
|
||||||
|
defer func() { testServer.Close() }()
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, expectedPlugins, plugins)
|
||||||
|
|
||||||
|
ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
newPlugin.InstalledVersion = manifest.Version
|
||||||
|
require.Equal(t, expectedPlugins, plugins)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||||
|
samplePlugins := []*model.MarketplacePlugin{
|
||||||
|
{
|
||||||
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
|
HomepageURL: "https://github.com/mattermost/mattermost-plugin-nps",
|
||||||
|
IconData: "Cjxzdmcgdmlld0JveD0nMCAwIDEwNSA5MycgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz4KPHBhdGggZD0nTTY2LDBoMzl2OTN6TTM4LDBoLTM4djkzek01MiwzNWwyNSw1OGgtMTZsLTgtMThoLTE4eicgZmlsbD0nI0VEMUMyNCcvPgo8L3N2Zz4K",
|
||||||
|
DownloadURL: "https://github.com/mattermost/mattermost-plugin-nps/releases/download/v1.0.3/com.mattermost.nps-1.0.3.tar.gz",
|
||||||
|
Manifest: &model.Manifest{
|
||||||
|
Id: "com.mattermost.nps",
|
||||||
|
Name: "User Satisfaction Surveys",
|
||||||
|
Description: "This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost.",
|
||||||
|
Version: "1.0.3",
|
||||||
|
MinServerVersion: "5.14.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InstalledVersion: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
path, _ := fileutils.FindDir("tests")
|
||||||
|
tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tarDataV2, err := ioutil.ReadFile(filepath.Join(path, "testpluginv2.tar.gz"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("search installed plugin", func(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
res.WriteHeader(http.StatusOK)
|
||||||
|
json, err := json.Marshal(samplePlugins)
|
||||||
|
require.NoError(t, err)
|
||||||
|
res.Write(json)
|
||||||
|
}))
|
||||||
|
defer func() { testServer.Close() }()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.PluginSettings.Enable = true
|
||||||
|
*cfg.PluginSettings.EnableUploads = true
|
||||||
|
*cfg.PluginSettings.EnableMarketplace = true
|
||||||
|
*cfg.PluginSettings.MarketplaceUrl = testServer.URL
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, samplePlugins, plugins)
|
||||||
|
|
||||||
|
manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData))
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
newPluginV1 := &model.MarketplacePlugin{
|
||||||
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
|
HomepageURL: "",
|
||||||
|
IconData: "",
|
||||||
|
DownloadURL: "",
|
||||||
|
Manifest: manifest,
|
||||||
|
},
|
||||||
|
InstalledVersion: manifest.Version,
|
||||||
|
}
|
||||||
|
expectedPlugins := append(samplePlugins, newPluginV1)
|
||||||
|
|
||||||
|
manifest, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarDataV2))
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
newPluginV2 := &model.MarketplacePlugin{
|
||||||
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
|
HomepageURL: "",
|
||||||
|
IconData: "",
|
||||||
|
DownloadURL: "",
|
||||||
|
Manifest: manifest,
|
||||||
|
},
|
||||||
|
InstalledVersion: manifest.Version,
|
||||||
|
}
|
||||||
|
expectedPlugins = append(expectedPlugins, newPluginV2)
|
||||||
|
sort.SliceStable(expectedPlugins, func(i, j int) bool {
|
||||||
|
return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, expectedPlugins, plugins)
|
||||||
|
|
||||||
|
// Search for plugins from the server
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "testplugin_v2"})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, []*model.MarketplacePlugin{newPluginV2}, plugins)
|
||||||
|
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "dsgsdg_v2"})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, []*model.MarketplacePlugin{newPluginV2}, plugins)
|
||||||
|
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "User Satisfaction Surveys"})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Equal(t, samplePlugins, plugins)
|
||||||
|
|
||||||
|
plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "NOFILTER"})
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
require.Nil(t, plugins)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func findClusterMessages(event string, msgs []*model.ClusterMessage) []*model.ClusterMessage {
|
func findClusterMessages(event string, msgs []*model.ClusterMessage) []*model.ClusterMessage {
|
||||||
var result []*model.ClusterMessage
|
var result []*model.ClusterMessage
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
|
|||||||
@@ -602,6 +602,8 @@ func (a *App) trackConfig() {
|
|||||||
"enable_uploads": *cfg.PluginSettings.EnableUploads,
|
"enable_uploads": *cfg.PluginSettings.EnableUploads,
|
||||||
"allow_insecure_download_url": *cfg.PluginSettings.AllowInsecureDownloadUrl,
|
"allow_insecure_download_url": *cfg.PluginSettings.AllowInsecureDownloadUrl,
|
||||||
"enable_health_check": *cfg.PluginSettings.EnableHealthCheck,
|
"enable_health_check": *cfg.PluginSettings.EnableHealthCheck,
|
||||||
|
"enable_marketplace": *cfg.PluginSettings.EnableMarketplace,
|
||||||
|
"is_default_marketplace_url": isDefault(*cfg.PluginSettings.MarketplaceUrl, model.PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL),
|
||||||
})
|
})
|
||||||
|
|
||||||
a.SendDiagnostic(TRACK_CONFIG_DATA_RETENTION, map[string]interface{}{
|
a.SendDiagnostic(TRACK_CONFIG_DATA_RETENTION, map[string]interface{}{
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/mlog"
|
"github.com/mattermost/mattermost-server/mlog"
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/plugin"
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
"github.com/mattermost/mattermost-server/services/filesstore"
|
"github.com/mattermost/mattermost-server/services/filesstore"
|
||||||
|
"github.com/mattermost/mattermost-server/services/marketplace"
|
||||||
"github.com/mattermost/mattermost-server/utils/fileutils"
|
"github.com/mattermost/mattermost-server/utils/fileutils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -400,6 +402,103 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
|||||||
return resp, nil
|
return resp, 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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
Manifest: plugin.Manifest,
|
||||||
|
},
|
||||||
|
InstalledVersion: plugin.Manifest.Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort result alphabetically.
|
||||||
|
sort.SliceStable(result, func(i, j int) bool {
|
||||||
|
return strings.ToLower(result[i].Manifest.Name) < strings.ToLower(result[j].Manifest.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginMatchesFilter(manifest *model.Manifest, filter string) bool {
|
||||||
|
filter = strings.TrimSpace(strings.ToLower(filter))
|
||||||
|
|
||||||
|
if filter == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(manifest.Id) == filter {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(manifest.Name), filter) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(manifest.Description), filter) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// notifyPluginEnabled notifies connected websocket clients across all peers if the version of the given
|
// notifyPluginEnabled notifies connected websocket clients across all peers if the version of the given
|
||||||
// plugin is same across them.
|
// plugin is same across them.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
|
|||||||
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
|
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
|
||||||
props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
|
props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
|
||||||
props["EnableSVGs"] = strconv.FormatBool(*c.ServiceSettings.EnableSVGs)
|
props["EnableSVGs"] = strconv.FormatBool(*c.ServiceSettings.EnableSVGs)
|
||||||
|
props["EnableMarketplace"] = strconv.FormatBool(*c.PluginSettings.EnableMarketplace)
|
||||||
|
|
||||||
// This setting is only temporary, so keep using the old setting name for the mobile and web apps
|
// This setting is only temporary, so keep using the old setting name for the mobile and web apps
|
||||||
props["ExperimentalEnablePostMetadata"] = "true"
|
props["ExperimentalEnablePostMetadata"] = "true"
|
||||||
|
|||||||
16
i18n/en.json
16
i18n/en.json
@@ -3506,6 +3506,22 @@
|
|||||||
"id": "app.plugin.manifest.app_error",
|
"id": "app.plugin.manifest.app_error",
|
||||||
"translation": "Unable to find manifest for extracted plugin"
|
"translation": "Unable to find manifest for extracted plugin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.plugin.marketplace_client.app_error",
|
||||||
|
"translation": "Failed to create marketplace client."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.plugin.marketplace_disabled.app_error",
|
||||||
|
"translation": "Marketplace has been disabled. Please check your logs for details."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.plugin.marketplace_plugins.app_error",
|
||||||
|
"translation": "Failed to get plugins from the marketplace server."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.plugin.marshal.app_error",
|
||||||
|
"translation": "Failed to marshal marketplace plugins."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.plugin.mvdir.app_error",
|
"id": "app.plugin.mvdir.app_error",
|
||||||
"translation": "Unable to move plugin from temporary directory to final destination. Another plugin may be using the same directory name."
|
"translation": "Unable to move plugin from temporary directory to final destination. Another plugin may be using the same directory name."
|
||||||
|
|||||||
@@ -4519,6 +4519,31 @@ func (c *Client4) DisablePlugin(id string) (bool, *Response) {
|
|||||||
return CheckStatusOK(r), BuildResponse(r)
|
return CheckStatusOK(r), BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMarketplacePlugins will return a list of plugins that an admin can install.
|
||||||
|
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
|
||||||
|
func (c *Client4) GetMarketplacePlugins(filter *MarketplacePluginFilter) ([]*MarketplacePlugin, *Response) {
|
||||||
|
route := c.GetPluginsRoute() + "/marketplace"
|
||||||
|
u, parseErr := url.Parse(route)
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, &Response{Error: NewAppError("GetMarketplacePlugins", "model.client.parse_plugins.app_error", nil, parseErr.Error(), http.StatusBadRequest)}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.ApplyToURL(u)
|
||||||
|
|
||||||
|
r, err := c.DoApiGet(u.String(), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
plugins, readerErr := MarketplacePluginsFromReader(r.Body)
|
||||||
|
if readerErr != nil {
|
||||||
|
return nil, BuildErrorResponse(r, NewAppError(route, "model.client.parse_plugins.app_error", nil, err.Error(), http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins, BuildResponse(r)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateChannelScheme will update a channel's scheme.
|
// UpdateChannelScheme will update a channel's scheme.
|
||||||
func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (bool, *Response) {
|
func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (bool, *Response) {
|
||||||
sip := &SchemeIDPatch{SchemeID: &schemeId}
|
sip := &SchemeIDPatch{SchemeID: &schemeId}
|
||||||
|
|||||||
@@ -171,8 +171,10 @@ const (
|
|||||||
DATA_RETENTION_SETTINGS_DEFAULT_FILE_RETENTION_DAYS = 365
|
DATA_RETENTION_SETTINGS_DEFAULT_FILE_RETENTION_DAYS = 365
|
||||||
DATA_RETENTION_SETTINGS_DEFAULT_DELETION_JOB_START_TIME = "02:00"
|
DATA_RETENTION_SETTINGS_DEFAULT_DELETION_JOB_START_TIME = "02:00"
|
||||||
|
|
||||||
PLUGIN_SETTINGS_DEFAULT_DIRECTORY = "./plugins"
|
PLUGIN_SETTINGS_DEFAULT_DIRECTORY = "./plugins"
|
||||||
PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY = "./client/plugins"
|
PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY = "./client/plugins"
|
||||||
|
PLUGIN_SETTINGS_DEFAULT_ENABLE_MARKETPLACE = true
|
||||||
|
PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL = "https://marketplace.integrations.mattermost.com"
|
||||||
|
|
||||||
COMPLIANCE_EXPORT_TYPE_CSV = "csv"
|
COMPLIANCE_EXPORT_TYPE_CSV = "csv"
|
||||||
COMPLIANCE_EXPORT_TYPE_ACTIANCE = "actiance"
|
COMPLIANCE_EXPORT_TYPE_ACTIANCE = "actiance"
|
||||||
@@ -2201,6 +2203,8 @@ type PluginSettings struct {
|
|||||||
ClientDirectory *string `restricted:"true"`
|
ClientDirectory *string `restricted:"true"`
|
||||||
Plugins map[string]map[string]interface{}
|
Plugins map[string]map[string]interface{}
|
||||||
PluginStates map[string]*PluginState
|
PluginStates map[string]*PluginState
|
||||||
|
EnableMarketplace *bool
|
||||||
|
MarketplaceUrl *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginSettings) SetDefaults(ls LogSettings) {
|
func (s *PluginSettings) SetDefaults(ls LogSettings) {
|
||||||
@@ -2220,22 +2224,14 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) {
|
|||||||
s.EnableHealthCheck = NewBool(true)
|
s.EnableHealthCheck = NewBool(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Directory == nil {
|
if s.Directory == nil || *s.Directory == "" {
|
||||||
s.Directory = NewString(PLUGIN_SETTINGS_DEFAULT_DIRECTORY)
|
s.Directory = NewString(PLUGIN_SETTINGS_DEFAULT_DIRECTORY)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *s.Directory == "" {
|
if s.ClientDirectory == nil || *s.ClientDirectory == "" {
|
||||||
*s.Directory = PLUGIN_SETTINGS_DEFAULT_DIRECTORY
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ClientDirectory == nil {
|
|
||||||
s.ClientDirectory = NewString(PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY)
|
s.ClientDirectory = NewString(PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *s.ClientDirectory == "" {
|
|
||||||
*s.ClientDirectory = PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Plugins == nil {
|
if s.Plugins == nil {
|
||||||
s.Plugins = make(map[string]map[string]interface{})
|
s.Plugins = make(map[string]map[string]interface{})
|
||||||
}
|
}
|
||||||
@@ -2248,6 +2244,14 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) {
|
|||||||
// Enable the NPS plugin by default if diagnostics are enabled
|
// Enable the NPS plugin by default if diagnostics are enabled
|
||||||
s.PluginStates["com.mattermost.nps"] = &PluginState{Enable: ls.EnableDiagnostics == nil || *ls.EnableDiagnostics}
|
s.PluginStates["com.mattermost.nps"] = &PluginState{Enable: ls.EnableDiagnostics == nil || *ls.EnableDiagnostics}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.EnableMarketplace == nil {
|
||||||
|
s.EnableMarketplace = NewBool(PLUGIN_SETTINGS_DEFAULT_ENABLE_MARKETPLACE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MarketplaceUrl == nil || *s.MarketplaceUrl == "" {
|
||||||
|
s.MarketplaceUrl = NewString(PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type GlobalRelayMessageExportSettings struct {
|
type GlobalRelayMessageExportSettings struct {
|
||||||
|
|||||||
69
model/marketplace_plugin.go
Обычный файл
69
model/marketplace_plugin.go
Обычный файл
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseMarketplacePlugin is a Mattermost plugin received from the marketplace server.
|
||||||
|
type BaseMarketplacePlugin struct {
|
||||||
|
HomepageURL string `json:"homepage_url"`
|
||||||
|
DownloadURL string `json:"download_url"`
|
||||||
|
IconData string `json:"icon_data"`
|
||||||
|
Manifest *Manifest `json:"manifest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarketplacePlugin is a state aware marketplace plugin.
|
||||||
|
type MarketplacePlugin struct {
|
||||||
|
*BaseMarketplacePlugin
|
||||||
|
InstalledVersion string `json:"installed_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseMarketplacePluginsFromReader decodes a json-encoded list of plugins from the given io.Reader.
|
||||||
|
func BaseMarketplacePluginsFromReader(reader io.Reader) ([]*BaseMarketplacePlugin, error) {
|
||||||
|
plugins := []*BaseMarketplacePlugin{}
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
|
||||||
|
if err := decoder.Decode(&plugins); err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarketplacePluginsFromReader decodes a json-encoded list of plugins from the given io.Reader.
|
||||||
|
func MarketplacePluginsFromReader(reader io.Reader) ([]*MarketplacePlugin, error) {
|
||||||
|
plugins := []*MarketplacePlugin{}
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
|
||||||
|
if err := decoder.Decode(&plugins); err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarketplacePluginFilter describes the parameters to request a list of plugins.
|
||||||
|
type MarketplacePluginFilter struct {
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
Filter string
|
||||||
|
ServerVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyToURL modifies the given url to include query string parameters for the request.
|
||||||
|
func (filter *MarketplacePluginFilter) ApplyToURL(u *url.URL) {
|
||||||
|
q := u.Query()
|
||||||
|
q.Add("page", strconv.Itoa(filter.Page))
|
||||||
|
if filter.PerPage > 0 {
|
||||||
|
q.Add("per_page", strconv.Itoa(filter.PerPage))
|
||||||
|
}
|
||||||
|
q.Add("filter", filter.Filter)
|
||||||
|
q.Add("server_version", filter.ServerVersion)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("Item not found")
|
||||||
|
|
||||||
type apiImplCreatorFunc func(*model.Manifest) API
|
type apiImplCreatorFunc func(*model.Manifest) API
|
||||||
|
|
||||||
// registeredPlugin stores the state for a given plugin that has been activated
|
// registeredPlugin stores the state for a given plugin that has been activated
|
||||||
@@ -162,6 +164,23 @@ func (env *Environment) Statuses() (model.PluginStatuses, error) {
|
|||||||
return pluginStatuses, nil
|
return pluginStatuses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetManifest returns a manifest for a given pluginId.
|
||||||
|
// Returns ErrNotFound if plugin is not found.
|
||||||
|
func (env *Environment) GetManifest(pluginId string) (*model.Manifest, error) {
|
||||||
|
plugins, err := env.Available()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unable to get plugin statuses")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
|
||||||
|
return plugin.Manifest, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
|
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
|
||||||
// Check if we are already active
|
// Check if we are already active
|
||||||
if env.IsActive(id) {
|
if env.IsActive(id) {
|
||||||
|
|||||||
79
services/marketplace/client.go
Обычный файл
79
services/marketplace/client.go
Обычный файл
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package marketplace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
"github.com/mattermost/mattermost-server/services/httpservice"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the programmatic interface to the marketplace server API.
|
||||||
|
type Client struct {
|
||||||
|
address string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a client to the marketplace server at the given address.
|
||||||
|
func NewClient(address string, httpService httpservice.HTTPService) (*Client, error) {
|
||||||
|
var httpClient *http.Client
|
||||||
|
addressUrl, err := url.Parse(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to parse marketplace address")
|
||||||
|
}
|
||||||
|
if addressUrl.Hostname() == "localhost" || addressUrl.Hostname() == "127.0.0.1" {
|
||||||
|
httpClient = httpService.MakeClient(true)
|
||||||
|
} else {
|
||||||
|
httpClient = httpService.MakeClient(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
address: address,
|
||||||
|
httpClient: httpClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlugins fetches the list of plugins from the configured server.
|
||||||
|
func (c *Client) GetPlugins(request *model.MarketplacePluginFilter) ([]*model.BaseMarketplacePlugin, error) {
|
||||||
|
u, err := url.Parse(c.buildURL("/api/v1/plugins"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.ApplyToURL(u)
|
||||||
|
|
||||||
|
resp, err := c.doGet(u.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer closeBody(resp)
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
return model.BaseMarketplacePluginsFromReader(resp.Body)
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("failed with status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeBody ensures the Body of an http.Response is properly closed.
|
||||||
|
func closeBody(r *http.Response) {
|
||||||
|
if r.Body != nil {
|
||||||
|
_, _ = ioutil.ReadAll(r.Body)
|
||||||
|
_ = r.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) buildURL(urlPath string, args ...interface{}) string {
|
||||||
|
return fmt.Sprintf("%s%s", c.address, fmt.Sprintf(urlPath, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doGet(u string) (*http.Response, error) {
|
||||||
|
return c.httpClient.Get(u)
|
||||||
|
}
|
||||||
Двоичные данные
tests/testpluginv2.tar.gz
Обычный файл
Двоичные данные
tests/testpluginv2.tar.gz
Обычный файл
Двоичный файл не отображается.
Ссылка в новой задаче
Block a user