MM-10702 Moving plugins to use hashicorp go-plugin. (#8978)

* Moving plugins to use hashicorp go-plugin.

* Tweaks from feedback.
Этот коммит содержится в:
Christopher Speller
2018-06-25 12:33:13 -07:00
коммит произвёл GitHub
родитель ecefa6cdd1
Коммит 1e5c432e10
303 изменённых файлов: 52150 добавлений и 9404 удалений

Просмотреть файл

@@ -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,
})
}
}

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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()

Просмотреть файл

@@ -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) {