MM-10702 Moving plugins to use hashicorp go-plugin. (#8978)
* Moving plugins to use hashicorp go-plugin. * Tweaks from feedback.
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ecefa6cdd1
Коммит
1e5c432e10
10
app/app.go
10
app/app.go
@@ -24,7 +24,7 @@ import (
|
||||
tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
@@ -41,10 +41,8 @@ type App struct {
|
||||
|
||||
Log *mlog.Logger
|
||||
|
||||
PluginEnv *pluginenv.Environment
|
||||
PluginConfigListenerId string
|
||||
IsPluginSandboxSupported bool
|
||||
pluginStatuses map[string]*model.PluginStatus
|
||||
Plugins *plugin.Environment
|
||||
PluginConfigListenerId string
|
||||
|
||||
EmailBatching *EmailBatchingJob
|
||||
|
||||
@@ -231,8 +229,6 @@ func New(options ...Option) (outApp *App, outErr error) {
|
||||
handlers: make(map[string]webSocketHandler),
|
||||
}
|
||||
|
||||
app.initBuiltInPlugins()
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
@@ -35,7 +33,6 @@ type TestHelper struct {
|
||||
|
||||
tempConfigPath string
|
||||
tempWorkspace string
|
||||
pluginHooks map[string]plugin.Hooks
|
||||
}
|
||||
|
||||
type persistentTestStore struct {
|
||||
@@ -93,7 +90,6 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
|
||||
th := &TestHelper{
|
||||
App: a,
|
||||
pluginHooks: make(map[string]plugin.Hooks),
|
||||
tempConfigPath: tempConfig.Name(),
|
||||
}
|
||||
|
||||
@@ -123,6 +119,19 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
th.App.SetLicense(nil)
|
||||
}
|
||||
|
||||
if th.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.tempWorkspace = dir
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(th.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(th.tempWorkspace, "webapp")
|
||||
|
||||
th.App.InitPlugins(pluginDir, webappDir)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
@@ -364,65 +373,6 @@ func (me *TestHelper) TearDown() {
|
||||
}
|
||||
}
|
||||
|
||||
type mockPluginSupervisor struct {
|
||||
hooks plugin.Hooks
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Start(api plugin.API) error {
|
||||
return s.hooks.OnActivate(api)
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Wait() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Hooks() plugin.Hooks {
|
||||
return s.hooks
|
||||
}
|
||||
|
||||
func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks) {
|
||||
if me.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
me.tempWorkspace = dir
|
||||
}
|
||||
|
||||
manifestCopy := *manifest
|
||||
if manifestCopy.Backend == nil {
|
||||
manifestCopy.Backend = &model.ManifestBackend{}
|
||||
}
|
||||
manifestBytes, err := json.Marshal(&manifestCopy)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(me.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(me.tempWorkspace, "webapp")
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(pluginDir, manifest.Id), 0700); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(pluginDir, manifest.Id, "plugin.json"), manifestBytes, 0600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) {
|
||||
if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok {
|
||||
return &mockPluginSupervisor{hooks}, nil
|
||||
}
|
||||
return pluginenv.DefaultSupervisorProvider(bundle)
|
||||
})
|
||||
|
||||
me.pluginHooks[manifest.Id] = hooks
|
||||
}
|
||||
|
||||
func (me *TestHelper) ResetRoleMigration() {
|
||||
if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Roles"); err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -559,59 +559,60 @@ func (a *App) trackLicense() {
|
||||
}
|
||||
|
||||
func (a *App) trackPlugins() {
|
||||
if *a.Config().PluginSettings.Enable {
|
||||
totalActiveCount := -1 // -1 to indicate disabled or error
|
||||
webappActiveCount := 0
|
||||
backendActiveCount := 0
|
||||
totalInactiveCount := -1 // -1 to indicate disabled or error
|
||||
webappInactiveCount := 0
|
||||
backendInactiveCount := 0
|
||||
if a.PluginsReady() {
|
||||
totalEnabledCount := 0
|
||||
webappEnabledCount := 0
|
||||
backendEnabledCount := 0
|
||||
totalDisabledCount := 0
|
||||
webappDisabledCount := 0
|
||||
backendDisabledCount := 0
|
||||
brokenManifestCount := 0
|
||||
settingsCount := 0
|
||||
|
||||
plugins, _ := a.GetPlugins()
|
||||
pluginStates := a.Config().PluginSettings.PluginStates
|
||||
plugins, _ := a.Plugins.Available()
|
||||
|
||||
if plugins != nil {
|
||||
totalActiveCount = len(plugins.Active)
|
||||
|
||||
for _, plugin := range plugins.Active {
|
||||
if plugin.Webapp != nil {
|
||||
webappActiveCount += 1
|
||||
if pluginStates != nil && plugins != nil {
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
brokenManifestCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if plugin.Backend != nil {
|
||||
backendActiveCount += 1
|
||||
if state, ok := pluginStates[plugin.Manifest.Id]; ok && state.Enable {
|
||||
totalEnabledCount += 1
|
||||
if plugin.Manifest.Backend != nil {
|
||||
backendEnabledCount += 1
|
||||
}
|
||||
if plugin.Manifest.Webapp != nil {
|
||||
webappEnabledCount += 1
|
||||
}
|
||||
} else {
|
||||
totalDisabledCount += 1
|
||||
if plugin.Manifest.Backend != nil {
|
||||
backendDisabledCount += 1
|
||||
}
|
||||
if plugin.Manifest.Webapp != nil {
|
||||
webappDisabledCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.SettingsSchema != nil {
|
||||
settingsCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
totalInactiveCount = len(plugins.Inactive)
|
||||
|
||||
for _, plugin := range plugins.Inactive {
|
||||
if plugin.Webapp != nil {
|
||||
webappInactiveCount += 1
|
||||
}
|
||||
|
||||
if plugin.Backend != nil {
|
||||
backendInactiveCount += 1
|
||||
}
|
||||
|
||||
if plugin.SettingsSchema != nil {
|
||||
if plugin.Manifest.SettingsSchema != nil {
|
||||
settingsCount += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
totalEnabledCount = -1 // -1 to indicate disabled or error
|
||||
totalDisabledCount = -1 // -1 to indicate disabled or error
|
||||
}
|
||||
|
||||
a.SendDiagnostic(TRACK_PLUGINS, map[string]interface{}{
|
||||
"active_plugins": totalActiveCount,
|
||||
"active_webapp_plugins": webappActiveCount,
|
||||
"active_backend_plugins": backendActiveCount,
|
||||
"inactive_plugins": totalInactiveCount,
|
||||
"inactive_webapp_plugins": webappInactiveCount,
|
||||
"inactive_backend_plugins": backendInactiveCount,
|
||||
"plugins_with_settings": settingsCount,
|
||||
"enabled_plugins": totalEnabledCount,
|
||||
"enabled_webapp_plugins": webappEnabledCount,
|
||||
"enabled_backend_plugins": backendEnabledCount,
|
||||
"disabled_plugins": totalDisabledCount,
|
||||
"disabled_webapp_plugins": webappDisabledCount,
|
||||
"disabled_backend_plugins": backendDisabledCount,
|
||||
"plugins_with_settings": settingsCount,
|
||||
"plugins_with_broken_manifests": brokenManifestCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
883
app/plugin.go
883
app/plugin.go
@@ -4,356 +4,135 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
|
||||
builtinplugin "github.com/mattermost/mattermost-server/app/plugin"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/jira"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/ldapextras"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/zoom"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox"
|
||||
)
|
||||
|
||||
var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func(string) ([]byte, error){
|
||||
"jira": jira.Asset,
|
||||
"zoom": zoom.Asset,
|
||||
}
|
||||
|
||||
func (a *App) notifyPluginStatusesChanged() error {
|
||||
pluginStatuses, err := a.GetClusterPluginStatuses()
|
||||
if err != nil {
|
||||
return err
|
||||
func (a *App) SyncPluginsActiveState() {
|
||||
if a.Plugins == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Notify any system admins.
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED, "", "", "", nil)
|
||||
message.Add("plugin_statuses", pluginStatuses)
|
||||
message.Broadcast.ContainsSensitiveData = true
|
||||
a.Publish(message)
|
||||
config := a.Config().PluginSettings
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) setPluginStatusState(id string, state int) error {
|
||||
if _, ok := a.pluginStatuses[id]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.pluginStatuses[id].State = state
|
||||
|
||||
return a.notifyPluginStatusesChanged()
|
||||
}
|
||||
|
||||
func (a *App) initBuiltInPlugins() {
|
||||
plugins := map[string]builtinplugin.Plugin{
|
||||
"ldapextras": &ldapextras.Plugin{},
|
||||
}
|
||||
for id, p := range plugins {
|
||||
mlog.Debug("Initializing built-in plugin", mlog.String("plugin_id", id))
|
||||
api := &BuiltInPluginAPI{
|
||||
id: id,
|
||||
router: a.Srv.Router.PathPrefix("/plugins/" + id).Subrouter(),
|
||||
app: a,
|
||||
if *config.Enable {
|
||||
availablePlugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
a.Log.Error("Unable to get available plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
p.Initialize(api)
|
||||
}
|
||||
a.AddConfigListener(func(before, after *model.Config) {
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
|
||||
// Deactivate any plugins that have been disabled.
|
||||
for _, plugin := range a.Plugins.Active() {
|
||||
// Determine if plugin is enabled
|
||||
pluginId := plugin.Manifest.Id
|
||||
pluginEnabled := false
|
||||
if state, ok := config.PluginStates[pluginId]; ok {
|
||||
pluginEnabled = state.Enable
|
||||
}
|
||||
|
||||
// If it's not enabled we need to deactivate it
|
||||
if !pluginEnabled {
|
||||
a.Plugins.Deactivate(pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
// Activate any plugins that have been enabled
|
||||
for _, plugin := range availablePlugins {
|
||||
if plugin.Manifest == nil {
|
||||
plugin.WrapLogger(a.Log).Error("Plugin manifest could not be loaded", mlog.Err(plugin.ManifestError))
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine if plugin is enabled
|
||||
pluginId := plugin.Manifest.Id
|
||||
pluginEnabled := false
|
||||
if state, ok := config.PluginStates[pluginId]; ok {
|
||||
pluginEnabled = state.Enable
|
||||
}
|
||||
|
||||
// Activate plugin if enabled
|
||||
if pluginEnabled {
|
||||
if err := a.Plugins.Activate(pluginId); err != nil {
|
||||
plugin.WrapLogger(a.Log).Error("Unable to activate plugin", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // If plugins are disabled, shutdown plugins.
|
||||
a.Plugins.Shutdown()
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) NewPluginAPI(manifest *model.Manifest) plugin.API {
|
||||
return NewPluginAPI(a, manifest)
|
||||
}
|
||||
|
||||
func (a *App) InitPlugins(pluginDir, webappPluginDir string) {
|
||||
if a.Plugins != nil || !*a.Config().PluginSettings.Enable {
|
||||
a.SyncPluginsActiveState()
|
||||
return
|
||||
}
|
||||
|
||||
a.Log.Info("Starting up plugins")
|
||||
|
||||
if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Mkdir(webappPluginDir, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if env, err := plugin.NewEnvironment(a.NewPluginAPI, pluginDir, webappPluginDir, a.Log); err != nil {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
} else {
|
||||
a.Plugins = env
|
||||
}
|
||||
|
||||
// Sync plugin active state when config changes. Also notify plugins.
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = a.AddConfigListener(func(*model.Config, *model.Config) {
|
||||
a.SyncPluginsActiveState()
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnConfigurationChange()
|
||||
return true
|
||||
}, plugin.OnConfigurationChangeId)
|
||||
})
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
}
|
||||
|
||||
a.SyncPluginsActiveState()
|
||||
}
|
||||
|
||||
func (a *App) setPluginsActive(activate bool) {
|
||||
if a.PluginEnv == nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginsActive(%t): plugin env not initialized", activate))
|
||||
func (a *App) ShutDownPlugins() {
|
||||
if a.Plugins == nil {
|
||||
return
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginsActive(%t)", activate), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Info("Shutting down plugins")
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
a.Plugins.Shutdown()
|
||||
|
||||
enabled := false
|
||||
if state, ok := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]; ok {
|
||||
enabled = state.Enable
|
||||
}
|
||||
|
||||
a.pluginStatuses[plugin.Manifest.Id] = &model.PluginStatus{
|
||||
ClusterId: a.GetClusterId(),
|
||||
PluginId: plugin.Manifest.Id,
|
||||
PluginPath: filepath.Dir(plugin.ManifestPath),
|
||||
IsSandboxed: a.IsPluginSandboxSupported,
|
||||
Name: plugin.Manifest.Name,
|
||||
Description: plugin.Manifest.Description,
|
||||
Version: plugin.Manifest.Version,
|
||||
}
|
||||
|
||||
if activate && enabled {
|
||||
a.setPluginActive(plugin, activate)
|
||||
} else if !activate {
|
||||
a.setPluginActive(plugin, activate)
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) setPluginActiveById(id string, activate bool) {
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginActiveById(%t)", activate), mlog.String("plugin_id", id), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest != nil && plugin.Manifest.Id == id {
|
||||
a.setPluginActive(plugin, activate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) setPluginActive(plugin *model.BundleInfo, activate bool) {
|
||||
if plugin.Manifest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := plugin.Manifest.Id
|
||||
|
||||
active := a.PluginEnv.IsPluginActive(id)
|
||||
|
||||
if activate {
|
||||
if !active {
|
||||
if err := a.activatePlugin(plugin.Manifest); err != nil {
|
||||
mlog.Error("Plugin failed to activate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError))
|
||||
}
|
||||
}
|
||||
|
||||
} else if !activate {
|
||||
if active {
|
||||
if err := a.deactivatePlugin(plugin.Manifest); err != nil {
|
||||
mlog.Error("Plugin failed to deactivate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError))
|
||||
}
|
||||
} else {
|
||||
if err := a.setPluginStatusState(plugin.Manifest.Id, model.PluginStateNotRunning); err != nil {
|
||||
mlog.Error("Plugin status state failed to update", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
mlog.Debug("Activating plugin", mlog.String("plugin_id", manifest.Id))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
onError := func(err error) {
|
||||
mlog.Debug("Plugin failed to stay running", mlog.String("plugin_id", manifest.Id), mlog.Err(err))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStayRunning); err != nil {
|
||||
mlog.Error("Failed to record plugin status", mlog.String("plugin_id", manifest.Id), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.PluginEnv.ActivatePlugin(manifest.Id, onError); err != nil {
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStart); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateRunning); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
mlog.Info("Activated plugin", mlog.String("plugin_id", manifest.Id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
mlog.Debug("Deactivating plugin", mlog.String("plugin_id", manifest.Id))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil {
|
||||
return model.NewAppError("deactivatePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
a.UnregisterPluginCommands(manifest.Id)
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateNotRunning); err != nil {
|
||||
return model.NewAppError("deactivatePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
mlog.Info("Deactivated plugin", mlog.String("plugin_id", manifest.Id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallPlugin unpacks and installs a plugin but does not activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile, false)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model.Manifest, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
_, isPrepackaged := prepackagedPlugins[manifest.Id]
|
||||
if isPrepackaged && !allowPrepackaged {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidId.String()}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
bundles, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(a.PluginEnv.SearchPath(), manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.pluginStatuses[manifest.Id] = &model.PluginStatus{
|
||||
ClusterId: a.GetClusterId(),
|
||||
PluginId: manifest.Id,
|
||||
PluginPath: pluginPath,
|
||||
State: model.PluginStateNotRunning,
|
||||
IsSandboxed: a.IsPluginSandboxSupported,
|
||||
IsPrepackaged: isPrepackaged,
|
||||
Name: manifest.Name,
|
||||
Description: manifest.Description,
|
||||
Version: manifest.Version,
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// GetPlugins returned the plugins installed on this server, including the manifests needed to
|
||||
// enable plugins with web functionality.
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := &model.PluginInfo{
|
||||
Manifest: *plugin.Manifest,
|
||||
}
|
||||
_, info.Prepackaged = prepackagedPlugins[plugin.Manifest.Id]
|
||||
if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) {
|
||||
resp.Active = append(resp.Active, info)
|
||||
} else {
|
||||
resp.Inactive = append(resp.Inactive, info)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = ""
|
||||
a.Plugins = nil
|
||||
}
|
||||
|
||||
func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins := a.PluginEnv.ActivePlugins()
|
||||
plugins := a.Plugins.Active()
|
||||
|
||||
manifests := make([]*model.Manifest, len(plugins))
|
||||
for i, plugin := range plugins {
|
||||
@@ -363,99 +142,14 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
// GetPluginStatuses returns the status for plugins installed on this server.
|
||||
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
if !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
pluginStatuses := make([]*model.PluginStatus, 0, len(a.pluginStatuses))
|
||||
for _, pluginStatus := range a.pluginStatuses {
|
||||
pluginStatuses = append(pluginStatuses, pluginStatus)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
|
||||
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
pluginStatuses, err := a.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.Cluster != nil && *a.Config().ClusterSettings.Enable {
|
||||
clusterPluginStatuses, err := a.Cluster.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return a.removePlugin(id, false)
|
||||
}
|
||||
|
||||
func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if _, ok := prepackagedPlugins[id]; ok && !allowPrepackaged {
|
||||
return model.NewAppError("removePlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
var pluginPath string
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
pluginPath = filepath.Dir(p.ManifestPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if a.PluginEnv.IsPluginActive(id) {
|
||||
err := a.deactivatePlugin(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.RemoveAll(pluginPath)
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
delete(a.pluginStatuses, manifest.Id)
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous
|
||||
// activation if inactive anywhere in the cluster.
|
||||
func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -472,14 +166,11 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
// This call will cause SyncPluginsActiveState to be called and the plugin to be activated
|
||||
if err := a.SaveConfig(a.Config(), true); err != nil {
|
||||
if err.Id == "ent.cluster.save_config.error" {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError)
|
||||
@@ -492,11 +183,11 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
|
||||
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
|
||||
func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -513,10 +204,6 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false}
|
||||
})
|
||||
@@ -528,331 +215,35 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) {
|
||||
if a.PluginEnv != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*a.Config().PluginSettings.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Info("Starting up plugins")
|
||||
|
||||
a.pluginStatuses = make(map[string]*model.PluginStatus)
|
||||
|
||||
if err := os.Mkdir(pluginPath, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Mkdir(webappPath, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
options := []pluginenv.Option{
|
||||
pluginenv.SearchPath(pluginPath),
|
||||
pluginenv.WebappPath(webappPath),
|
||||
pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
|
||||
return &PluginAPI{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
keyValueStore: &PluginKeyValueStore{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
a.IsPluginSandboxSupported = sandbox.CheckSupport() == nil
|
||||
|
||||
if supervisorOverride != nil {
|
||||
options = append(options, pluginenv.SupervisorProvider(supervisorOverride))
|
||||
} else if a.IsPluginSandboxSupported {
|
||||
options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider))
|
||||
} else {
|
||||
options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider))
|
||||
}
|
||||
|
||||
if env, err := pluginenv.New(options...); err != nil {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
} else {
|
||||
a.PluginEnv = env
|
||||
}
|
||||
|
||||
for id, asset := range prepackagedPlugins {
|
||||
if tarball, err := asset("plugin.tar.gz"); err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.Err(err))
|
||||
} else if tarball != nil {
|
||||
a.removePlugin(id, true)
|
||||
if _, err := a.installPlugin(bytes.NewReader(tarball), true); err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
if _, ok := a.Config().PluginSettings.PluginStates[id]; !ok && id != "zoom" {
|
||||
if err := a.EnablePlugin(id); err != nil {
|
||||
mlog.Error("Failed to enable prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = a.AddConfigListener(func(oldCfg *model.Config, cfg *model.Config) {
|
||||
if a.PluginEnv == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if *oldCfg.PluginSettings.Enable != *cfg.PluginSettings.Enable {
|
||||
a.setPluginsActive(*cfg.PluginSettings.Enable)
|
||||
} else {
|
||||
plugins := map[string]bool{}
|
||||
for id := range oldCfg.PluginSettings.PluginStates {
|
||||
plugins[id] = true
|
||||
}
|
||||
for id := range cfg.PluginSettings.PluginStates {
|
||||
plugins[id] = true
|
||||
}
|
||||
|
||||
for id := range plugins {
|
||||
oldPluginState := oldCfg.PluginSettings.PluginStates[id]
|
||||
pluginState := cfg.PluginSettings.PluginStates[id]
|
||||
|
||||
wasEnabled := oldPluginState != nil && oldPluginState.Enable
|
||||
isEnabled := pluginState != nil && pluginState.Enable
|
||||
|
||||
if wasEnabled != isEnabled {
|
||||
a.setPluginActiveById(id, isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, err := range a.PluginEnv.Hooks().OnConfigurationChange() {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
a.setPluginsActive(true)
|
||||
}
|
||||
|
||||
func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
|
||||
mlog.Error(err.Error())
|
||||
w.WriteHeader(err.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(err.ToJson()))
|
||||
return
|
||||
}
|
||||
|
||||
a.servePluginRequest(w, r, a.PluginEnv.Hooks().ServeHTTP)
|
||||
}
|
||||
|
||||
func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) {
|
||||
token := ""
|
||||
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") {
|
||||
token = authHeader[len(model.HEADER_BEARER)+1:]
|
||||
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+" ") {
|
||||
token = authHeader[len(model.HEADER_TOKEN)+1:]
|
||||
} else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) {
|
||||
token = cookie.Value
|
||||
} else {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
}
|
||||
|
||||
r.Header.Del("Mattermost-User-Id")
|
||||
if token != "" {
|
||||
if session, err := a.GetSession(token); session != nil && err == nil {
|
||||
r.Header.Set("Mattermost-User-Id", session.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, c := range cookies {
|
||||
if c.Name != model.SESSION_COOKIE_TOKEN {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
}
|
||||
r.Header.Del(model.HEADER_AUTH)
|
||||
r.Header.Del("Referer")
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
newQuery := r.URL.Query()
|
||||
newQuery.Del("access_token")
|
||||
r.URL.RawQuery = newQuery.Encode()
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/plugins/"+params["plugin_id"])
|
||||
|
||||
handler(w, r.WithContext(context.WithValue(r.Context(), "plugin_id", params["plugin_id"])))
|
||||
}
|
||||
|
||||
func (a *App) ShutDownPlugins() {
|
||||
if a.PluginEnv == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Info("Shutting down plugins")
|
||||
|
||||
for _, err := range a.PluginEnv.Shutdown() {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = ""
|
||||
a.PluginEnv = nil
|
||||
}
|
||||
|
||||
func getKeyHash(key string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(key))
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: pluginId,
|
||||
Key: getKeyHash(key),
|
||||
Value: value,
|
||||
}
|
||||
|
||||
result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv)
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) {
|
||||
result := <-a.Srv.Store.Plugin().Get(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
if result.Err.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
mlog.Error(result.Err.Error())
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
kv := result.Data.(*model.PluginKeyValue)
|
||||
|
||||
return kv.Value, nil
|
||||
}
|
||||
|
||||
func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
||||
result := <-a.Srv.Store.Plugin().Delete(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
type PluginCommand struct {
|
||||
Command *model.Command
|
||||
PluginId string
|
||||
}
|
||||
|
||||
func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) error {
|
||||
if command.Trigger == "" {
|
||||
return fmt.Errorf("invalid command")
|
||||
}
|
||||
|
||||
command = &model.Command{
|
||||
Trigger: strings.ToLower(command.Trigger),
|
||||
TeamId: command.TeamId,
|
||||
AutoComplete: command.AutoComplete,
|
||||
AutoCompleteDesc: command.AutoCompleteDesc,
|
||||
AutoCompleteHint: command.AutoCompleteHint,
|
||||
DisplayName: command.DisplayName,
|
||||
}
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
|
||||
if pc.PluginId == pluginId {
|
||||
pc.Command = command
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.pluginCommands = append(a.pluginCommands, &PluginCommand{
|
||||
Command: command,
|
||||
PluginId: pluginId,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommand(pluginId, teamId, trigger string) {
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId != teamId || pc.Command.Trigger != trigger {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommands(pluginId string) {
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.PluginId != pluginId {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) PluginCommandsForTeam(teamId string) []*model.Command {
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
var commands []*model.Command
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId == "" || pc.Command.TeamId == teamId {
|
||||
commands = append(commands, pc.Command)
|
||||
}
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (a *App) ExecutePluginCommand(args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
parts := strings.Split(args.Command, " ")
|
||||
trigger := parts[0][1:]
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
|
||||
response, appErr, err := a.PluginEnv.HooksForPlugin(pc.PluginId).ExecuteCommand(args)
|
||||
if err != nil {
|
||||
return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return pc.Command, response, appErr
|
||||
}
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (a *App) PluginsReady() bool {
|
||||
return a.PluginEnv != nil && *a.Config().PluginSettings.Enable
|
||||
return a.Plugins != nil && *a.Config().PluginSettings.Enable
|
||||
}
|
||||
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
if !a.PluginsReady() {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
availablePlugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
|
||||
for _, plugin := range availablePlugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := &model.PluginInfo{
|
||||
Manifest: *plugin.Manifest,
|
||||
}
|
||||
|
||||
if a.Plugins.IsActive(plugin.Manifest.Id) {
|
||||
resp.Active = append(resp.Active, info)
|
||||
} else {
|
||||
resp.Inactive = append(resp.Inactive, info)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
// Loads the plugin's configuration
|
||||
LoadPluginConfiguration(dest interface{}) error
|
||||
|
||||
// The plugin's router
|
||||
PluginRouter() *mux.Router
|
||||
|
||||
// Gets a team by its name
|
||||
GetTeamByName(name string) (*model.Team, *model.AppError)
|
||||
|
||||
// Gets a user by its name
|
||||
GetUserByName(name string) (*model.User, *model.AppError)
|
||||
|
||||
// Gets a channel by its name
|
||||
GetChannelByName(teamId, name string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Gets a direct message channel
|
||||
GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Creates a post
|
||||
CreatePost(post *model.Post) (*model.Post, *model.AppError)
|
||||
|
||||
// Get LDAP attributes for a user
|
||||
GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError)
|
||||
|
||||
// Temporary for built-in plugins, copied from api4/context.go ServeHTTP function.
|
||||
// If a request has a valid token for an active session, the session is returned otherwise
|
||||
// it errors.
|
||||
GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError)
|
||||
|
||||
// Returns a localized string. If a request is given, its headers will be used to pick a locale.
|
||||
I18n(id string, r *http.Request) string
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// Base provides default implementations for hooks.
|
||||
type Base struct{}
|
||||
|
||||
func (b *Base) OnConfigurationChange() {}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// All implementations should be safe for concurrent use.
|
||||
type Hooks interface {
|
||||
// Invoked when configuration changes may have been made
|
||||
OnConfigurationChange()
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !amd64 !darwin,!linux,!windows
|
||||
|
||||
package jira
|
||||
|
||||
func Asset(name string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package ldapextras
|
||||
|
||||
type Configuration struct {
|
||||
Enabled bool
|
||||
Attributes []string
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package ldapextras
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/mattermost/mattermost-server/app/plugin"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
plugin.Base
|
||||
api plugin.API
|
||||
configuration atomic.Value
|
||||
}
|
||||
|
||||
func (p *Plugin) Initialize(api plugin.API) {
|
||||
p.api = api
|
||||
p.OnConfigurationChange()
|
||||
api.PluginRouter().HandleFunc("/users/{user_id:[A-Za-z0-9]+}/attributes", p.handleGetAttributes).Methods("GET")
|
||||
}
|
||||
|
||||
func (p *Plugin) config() *Configuration {
|
||||
return p.configuration.Load().(*Configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnConfigurationChange() {
|
||||
var configuration Configuration
|
||||
if err := p.api.LoadPluginConfiguration(&configuration); err != nil {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
p.configuration.Store(&configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleGetAttributes(w http.ResponseWriter, r *http.Request) {
|
||||
config := p.config()
|
||||
if !config.Enabled || len(config.Attributes) == 0 {
|
||||
http.Error(w, "This plugin is not configured", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := p.api.GetSessionFromRequest(r)
|
||||
|
||||
if session == nil || err != nil {
|
||||
http.Error(w, "Invalid session", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Only requires a valid session, no other permission checks required
|
||||
|
||||
params := mux.Vars(r)
|
||||
id := params["user_id"]
|
||||
|
||||
if len(id) != 26 {
|
||||
http.Error(w, "Invalid user id", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
attributes, err := p.api.GetLdapUserAttributes(id, config.Attributes)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Errored getting attributes: %v", err.Error()), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Write([]byte(model.MapToJson(attributes)))
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
type Plugin interface {
|
||||
Initialize(API)
|
||||
Hooks
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !amd64 !darwin,!linux,!windows
|
||||
|
||||
package zoom
|
||||
|
||||
func Asset(name string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -5,27 +5,22 @@ package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type PluginAPI struct {
|
||||
id string
|
||||
app *App
|
||||
keyValueStore *PluginKeyValueStore
|
||||
}
|
||||
|
||||
type PluginKeyValueStore struct {
|
||||
id string
|
||||
app *App
|
||||
}
|
||||
|
||||
func NewPluginAPI(a *App, manifest *model.Manifest) *PluginAPI {
|
||||
return &PluginAPI{
|
||||
id: manifest.Id,
|
||||
app: a,
|
||||
}
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
@@ -170,129 +165,14 @@ func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError
|
||||
return api.app.UpdatePost(post, false)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) KeyValueStore() plugin.KeyValueStore {
|
||||
return api.keyValueStore
|
||||
func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError {
|
||||
return api.app.SetPluginKey(api.id, key, value)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
return s.app.SetPluginKey(s.id, key, value)
|
||||
func (api *PluginAPI) KVGet(key string) ([]byte, *model.AppError) {
|
||||
return api.app.GetPluginKey(api.id, key)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
return s.app.GetPluginKey(s.id, key)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Delete(key string) *model.AppError {
|
||||
return s.app.DeletePluginKey(s.id, key)
|
||||
}
|
||||
|
||||
type BuiltInPluginAPI struct {
|
||||
id string
|
||||
router *mux.Router
|
||||
app *App
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) PluginRouter() *mux.Router {
|
||||
return api.router
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetUserByName(name string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetChannelByName(teamId, name string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError) {
|
||||
if api.app.Ldap == nil {
|
||||
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
user, err := api.app.GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.AuthData == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return api.app.Ldap.GetUserAttributes(*user.AuthData, attributes)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError) {
|
||||
token := ""
|
||||
isTokenFromQueryString := false
|
||||
|
||||
// Attempt to parse token out of the header
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
|
||||
// Default session token
|
||||
token = authHeader[7:]
|
||||
|
||||
} else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
|
||||
// OAuth token
|
||||
token = authHeader[6:]
|
||||
}
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if len(token) == 0 {
|
||||
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
token = cookie.Value
|
||||
|
||||
if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse token out of the query string
|
||||
if len(token) == 0 {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
isTokenFromQueryString = true
|
||||
}
|
||||
|
||||
if len(token) == 0 {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
session, err := api.app.GetSession(token)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
} else if !session.IsOAuth && isTokenFromQueryString {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) I18n(id string, r *http.Request) string {
|
||||
if r != nil {
|
||||
f, _ := utils.GetTranslationsAndLocale(nil, r)
|
||||
return f(id)
|
||||
}
|
||||
f, _ := utils.GetTranslationsBySystemLocale()
|
||||
return f(id)
|
||||
func (api *PluginAPI) KVDelete(key string) *model.AppError {
|
||||
return api.app.DeletePluginKey(api.id, key)
|
||||
}
|
||||
|
||||
112
app/plugin_commands.go
Обычный файл
112
app/plugin_commands.go
Обычный файл
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type PluginCommand struct {
|
||||
Command *model.Command
|
||||
PluginId string
|
||||
}
|
||||
|
||||
func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) error {
|
||||
if command.Trigger == "" {
|
||||
return fmt.Errorf("invalid command")
|
||||
}
|
||||
|
||||
command = &model.Command{
|
||||
Trigger: strings.ToLower(command.Trigger),
|
||||
TeamId: command.TeamId,
|
||||
AutoComplete: command.AutoComplete,
|
||||
AutoCompleteDesc: command.AutoCompleteDesc,
|
||||
AutoCompleteHint: command.AutoCompleteHint,
|
||||
DisplayName: command.DisplayName,
|
||||
}
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
|
||||
if pc.PluginId == pluginId {
|
||||
pc.Command = command
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.pluginCommands = append(a.pluginCommands, &PluginCommand{
|
||||
Command: command,
|
||||
PluginId: pluginId,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommand(pluginId, teamId, trigger string) {
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId != teamId || pc.Command.Trigger != trigger {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommands(pluginId string) {
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.PluginId != pluginId {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) PluginCommandsForTeam(teamId string) []*model.Command {
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
var commands []*model.Command
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId == "" || pc.Command.TeamId == teamId {
|
||||
commands = append(commands, pc.Command)
|
||||
}
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (a *App) ExecutePluginCommand(args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
parts := strings.Split(args.Command, " ")
|
||||
trigger := parts[0][1:]
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
|
||||
pluginHooks, err := a.Plugins.HooksForPlugin(pc.PluginId)
|
||||
if err != nil {
|
||||
return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
response, appErr := pluginHooks.ExecuteCommand(args)
|
||||
return pc.Command, response, appErr
|
||||
}
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
119
app/plugin_install.go
Обычный файл
119
app/plugin_install.go
Обычный файл
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidId.String()}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
bundles, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Check that there is no plugin with the same ID
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(*a.Config().PluginSettings.Directory, manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return a.removePlugin(id)
|
||||
}
|
||||
|
||||
func (a *App) removePlugin(id string) *model.AppError {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
var pluginPath string
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
pluginPath = filepath.Dir(p.ManifestPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
a.Plugins.Deactivate(id)
|
||||
|
||||
err = os.RemoveAll(pluginPath)
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
app/plugin_key_value_store.go
Обычный файл
61
app/plugin_key_value_store.go
Обычный файл
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func getKeyHash(key string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(key))
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: pluginId,
|
||||
Key: getKeyHash(key),
|
||||
Value: value,
|
||||
}
|
||||
|
||||
result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv)
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) {
|
||||
result := <-a.Srv.Store.Plugin().Get(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
if result.Err.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
mlog.Error(result.Err.Error())
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
kv := result.Data.(*model.PluginKeyValue)
|
||||
|
||||
return kv.Value, nil
|
||||
}
|
||||
|
||||
func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
||||
result := <-a.Srv.Store.Plugin().Delete(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
75
app/plugin_requests.go
Обычный файл
75
app/plugin_requests.go
Обычный файл
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
|
||||
a.Log.Error(err.Error())
|
||||
w.WriteHeader(err.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(err.ToJson()))
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
hooks, err := a.Plugins.HooksForPlugin(params["plugin_id"])
|
||||
if err != nil {
|
||||
a.Log.Error("Access to route for non-existant plugin", mlog.String("missing_plugin_id", params["plugin_id"]), mlog.Err(err))
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
a.servePluginRequest(w, r, hooks.ServeHTTP)
|
||||
}
|
||||
|
||||
func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) {
|
||||
token := ""
|
||||
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") {
|
||||
token = authHeader[len(model.HEADER_BEARER)+1:]
|
||||
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+" ") {
|
||||
token = authHeader[len(model.HEADER_TOKEN)+1:]
|
||||
} else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) {
|
||||
token = cookie.Value
|
||||
} else {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
}
|
||||
|
||||
r.Header.Del("Mattermost-User-Id")
|
||||
if token != "" {
|
||||
if session, err := a.GetSession(token); session != nil && err == nil {
|
||||
r.Header.Set("Mattermost-User-Id", session.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, c := range cookies {
|
||||
if c.Name != model.SESSION_COOKIE_TOKEN {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
}
|
||||
r.Header.Del(model.HEADER_AUTH)
|
||||
r.Header.Del("Referer")
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
newQuery := r.URL.Query()
|
||||
newQuery.Del("access_token")
|
||||
r.URL.RawQuery = newQuery.Encode()
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/plugins/"+params["plugin_id"])
|
||||
|
||||
handler(w, r)
|
||||
}
|
||||
63
app/plugin_statuses.go
Обычный файл
63
app/plugin_statuses.go
Обычный файл
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
// GetPluginStatuses returns the status for plugins installed on this server.
|
||||
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
pluginStatuses, err := a.Plugins.Statuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "Unable to get plugin statuses", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Add our cluster ID
|
||||
for _, status := range pluginStatuses {
|
||||
status.ClusterId = a.GetClusterId()
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
|
||||
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
pluginStatuses, err := a.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.Cluster != nil && *a.Config().ClusterSettings.Enable {
|
||||
clusterPluginStatuses, err := a.Cluster.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (a *App) notifyPluginStatusesChanged() error {
|
||||
pluginStatuses, err := a.GetClusterPluginStatuses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Notify any system admins.
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED, "", "", "", nil)
|
||||
message.Add("plugin_statuses", pluginStatuses)
|
||||
message.Broadcast.ContainsSensitiveData = true
|
||||
a.Publish(message)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,19 +4,14 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
)
|
||||
|
||||
func TestPluginKeyValueStore(t *testing.T) {
|
||||
@@ -103,152 +98,6 @@ func TestHandlePluginRequest(t *testing.T) {
|
||||
router.ServeHTTP(nil, r)
|
||||
}
|
||||
|
||||
type testPlugin struct {
|
||||
plugintest.Hooks
|
||||
}
|
||||
|
||||
func (p *testPlugin) OnConfigurationChange() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPlugin) OnDeactivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type pluginCommandTestPlugin struct {
|
||||
testPlugin
|
||||
|
||||
TeamId string
|
||||
}
|
||||
|
||||
func (p *pluginCommandTestPlugin) OnActivate(api plugin.API) error {
|
||||
if err := api.RegisterCommand(&model.Command{
|
||||
Trigger: "foo",
|
||||
TeamId: p.TeamId,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.RegisterCommand(&model.Command{
|
||||
Trigger: "foo2",
|
||||
TeamId: p.TeamId,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return api.UnregisterCommand(p.TeamId, "foo2")
|
||||
}
|
||||
|
||||
func (p *pluginCommandTestPlugin) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
if args.Command == "/foo" {
|
||||
return &model.CommandResponse{
|
||||
Text: "bar",
|
||||
}, nil
|
||||
}
|
||||
return nil, model.NewAppError("ExecuteCommand", "this is an error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestPluginCommands(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.InstallPlugin(&model.Manifest{
|
||||
Id: "foo",
|
||||
}, &pluginCommandTestPlugin{
|
||||
TeamId: th.BasicTeam.Id,
|
||||
})
|
||||
|
||||
require.Nil(t, th.App.EnablePlugin("foo"))
|
||||
|
||||
// Ideally, we would wait for the websocket activation event instead of just sleeping.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
pluginStatuses, err := th.App.GetPluginStatuses()
|
||||
require.Nil(t, err)
|
||||
found := false
|
||||
for _, pluginStatus := range pluginStatuses {
|
||||
if pluginStatus.PluginId == "foo" {
|
||||
require.Equal(t, model.PluginStateRunning, pluginStatus.State)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
||||
|
||||
resp, err := th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo2",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
|
||||
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "bar", resp.Text)
|
||||
|
||||
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo baz",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "this is an error", err.Message)
|
||||
assert.Nil(t, resp)
|
||||
|
||||
require.Nil(t, th.App.RemovePlugin("foo"))
|
||||
|
||||
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
}
|
||||
|
||||
type pluginBadActivation struct {
|
||||
testPlugin
|
||||
}
|
||||
|
||||
func (p *pluginBadActivation) OnActivate(api plugin.API) error {
|
||||
return errors.New("won't activate for some reason")
|
||||
}
|
||||
|
||||
func TestPluginBadActivation(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.InstallPlugin(&model.Manifest{
|
||||
Id: "foo",
|
||||
}, &pluginBadActivation{})
|
||||
|
||||
t.Run("EnablePlugin bad activation", func(t *testing.T) {
|
||||
err := th.App.EnablePlugin("foo")
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Ideally, we would wait for the websocket activation event instead of just
|
||||
// sleeping.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
pluginStatuses, err := th.App.GetPluginStatuses()
|
||||
require.Nil(t, err)
|
||||
found := false
|
||||
for _, pluginStatus := range pluginStatuses {
|
||||
if pluginStatus.PluginId == "foo" {
|
||||
require.Equal(t, model.PluginStateFailedToStart, pluginStatus.State)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPluginStatusesDisabled(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
31
app/post.go
31
app/post.go
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/dyatlov/go-opengraph/opengraph"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"golang.org/x/net/html/charset"
|
||||
@@ -161,10 +162,13 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
if newPost, rejectionReason := a.PluginEnv.Hooks().MessageWillBePosted(post); newPost == nil {
|
||||
var rejectionReason string
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
post, rejectionReason = hooks.MessageWillBePosted(post)
|
||||
return post != nil
|
||||
}, plugin.MessageWillBePostedId)
|
||||
if post == nil {
|
||||
return nil, model.NewAppError("createPost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
} else {
|
||||
post = newPost
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +181,10 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
a.PluginEnv.Hooks().MessageHasBeenPosted(rpost)
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenPosted(rpost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenPostedId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -386,10 +393,13 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
if pluginModifiedPost, rejectionReason := a.PluginEnv.Hooks().MessageWillBeUpdated(newPost, oldPost); pluginModifiedPost == nil {
|
||||
return nil, model.NewAppError("createPost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
} else {
|
||||
newPost = pluginModifiedPost
|
||||
var rejectionReason string
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
newPost, rejectionReason = hooks.MessageWillBeUpdated(newPost, oldPost)
|
||||
return post != nil
|
||||
}, plugin.MessageWillBeUpdatedId)
|
||||
if newPost == nil {
|
||||
return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +410,10 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
a.PluginEnv.Hooks().MessageHasBeenUpdated(newPost, oldPost)
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenUpdated(newPost, oldPost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenUpdatedId)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ func TestUpdatePostEditAt(t *testing.T) {
|
||||
} else if saved.EditAt == post.EditAt {
|
||||
t.Fatal("should have updated post.EditAt when updating post message")
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
|
||||
func TestUpdatePostTimeLimit(t *testing.T) {
|
||||
|
||||
Ссылка в новой задаче
Block a user