mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
349 lines
11 KiB
Go
349 lines
11 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/plugin"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
|
|
"github.com/mattermost/mattermost/server/v8/channels/product"
|
|
"github.com/mattermost/mattermost/server/v8/config"
|
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
|
|
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
|
|
)
|
|
|
|
const ServerKey product.ServiceKey = "server"
|
|
|
|
// licenseSvc is added to act as a starting point for future integrated products.
|
|
// It has the same signature and functionality with the license related APIs of the plugin-api.
|
|
type licenseSvc interface {
|
|
GetLicense() *model.License
|
|
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
|
|
RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError
|
|
}
|
|
|
|
// Channels contains all channels related state.
|
|
type Channels struct {
|
|
srv *Server
|
|
cfgSvc product.ConfigService
|
|
filestore filestore.FileBackend
|
|
exportFilestore filestore.FileBackend
|
|
licenseSvc licenseSvc
|
|
routerSvc *routerService
|
|
|
|
postActionCookieSecret []byte
|
|
|
|
pluginCommandsLock sync.RWMutex
|
|
pluginCommands []*PluginCommand
|
|
pluginsLock sync.RWMutex
|
|
pluginsEnvironment *plugin.Environment
|
|
pluginConfigListenerID string
|
|
pluginClusterLeaderListenerID string
|
|
|
|
imageProxy *imageproxy.ImageProxy
|
|
|
|
// cached counts that are used during notice condition validation
|
|
cachedPostCount int64
|
|
cachedUserCount int64
|
|
cachedDBMSVersion string
|
|
// previously fetched notices
|
|
cachedNotices model.ProductNotices
|
|
|
|
AccountMigration einterfaces.AccountMigrationInterface
|
|
Compliance einterfaces.ComplianceInterface
|
|
DataRetention einterfaces.DataRetentionInterface
|
|
MessageExport einterfaces.MessageExportInterface
|
|
Saml einterfaces.SamlInterface
|
|
Notification einterfaces.NotificationInterface
|
|
Ldap einterfaces.LdapInterface
|
|
|
|
// These are used to prevent concurrent upload requests
|
|
// for a given upload session which could cause inconsistencies
|
|
// and data corruption.
|
|
uploadLockMapMut sync.Mutex
|
|
uploadLockMap map[string]bool
|
|
|
|
imgDecoder *imaging.Decoder
|
|
imgEncoder *imaging.Encoder
|
|
|
|
dndTaskMut sync.Mutex
|
|
dndTask *model.ScheduledTask
|
|
|
|
postReminderMut sync.Mutex
|
|
postReminderTask *model.ScheduledTask
|
|
}
|
|
|
|
func init() {
|
|
product.RegisterProduct("channels", product.Manifest{
|
|
Initializer: func(services map[product.ServiceKey]any) (product.Product, error) {
|
|
return NewChannels(services)
|
|
},
|
|
Dependencies: map[product.ServiceKey]struct{}{
|
|
ServerKey: {},
|
|
product.ConfigKey: {},
|
|
product.LicenseKey: {},
|
|
product.FilestoreKey: {},
|
|
product.ExportFilestoreKey: {},
|
|
},
|
|
})
|
|
}
|
|
|
|
func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
|
|
s, ok := services[ServerKey].(*Server)
|
|
if !ok {
|
|
return nil, errors.New("server not passed")
|
|
}
|
|
ch := &Channels{
|
|
srv: s,
|
|
imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()),
|
|
uploadLockMap: map[string]bool{},
|
|
}
|
|
|
|
// To get another service:
|
|
// 1. Prepare the service interface
|
|
// 2. Add the field to *Channels
|
|
// 3. Add the service key to the slice.
|
|
// 4. Add a new case in the switch statement.
|
|
requiredServices := []product.ServiceKey{
|
|
product.ConfigKey,
|
|
product.LicenseKey,
|
|
product.FilestoreKey,
|
|
product.ExportFilestoreKey,
|
|
}
|
|
for _, svcKey := range requiredServices {
|
|
svc, ok := services[svcKey]
|
|
if !ok {
|
|
return nil, fmt.Errorf("Service %s not passed", svcKey)
|
|
}
|
|
switch svcKey {
|
|
// Keep adding more services here
|
|
case product.ConfigKey:
|
|
cfgSvc, ok := svc.(product.ConfigService)
|
|
if !ok {
|
|
return nil, errors.New("Config service did not satisfy ConfigSvc interface")
|
|
}
|
|
ch.cfgSvc = cfgSvc
|
|
case product.FilestoreKey:
|
|
filestore, ok := svc.(filestore.FileBackend)
|
|
if !ok {
|
|
return nil, errors.New("Filestore service did not satisfy FileBackend interface")
|
|
}
|
|
ch.filestore = filestore
|
|
case product.ExportFilestoreKey:
|
|
exportFilestore, ok := svc.(filestore.FileBackend)
|
|
if !ok {
|
|
return nil, errors.New("Export filestore service did not satisfy FileBackend interface")
|
|
}
|
|
ch.exportFilestore = exportFilestore
|
|
case product.LicenseKey:
|
|
svc, ok := svc.(licenseSvc)
|
|
if !ok {
|
|
return nil, errors.New("License service did not satisfy licenseSvc interface")
|
|
}
|
|
ch.licenseSvc = svc
|
|
}
|
|
}
|
|
// We are passing a partially filled Channels struct so that the enterprise
|
|
// methods can have access to app methods.
|
|
// Otherwise, passing server would mean it has to call s.Channels(),
|
|
// which would be nil at this point.
|
|
if complianceInterface != nil {
|
|
ch.Compliance = complianceInterface(New(ServerConnector(ch)))
|
|
}
|
|
if messageExportInterface != nil {
|
|
ch.MessageExport = messageExportInterface(New(ServerConnector(ch)))
|
|
}
|
|
if dataRetentionInterface != nil {
|
|
ch.DataRetention = dataRetentionInterface(New(ServerConnector(ch)))
|
|
}
|
|
if accountMigrationInterface != nil {
|
|
ch.AccountMigration = accountMigrationInterface(New(ServerConnector(ch)))
|
|
}
|
|
if ldapInterface != nil {
|
|
ch.Ldap = ldapInterface(New(ServerConnector(ch)))
|
|
}
|
|
if notificationInterface != nil {
|
|
ch.Notification = notificationInterface(New(ServerConnector(ch)))
|
|
}
|
|
if samlInterfaceNew != nil {
|
|
ch.Saml = samlInterfaceNew(New(ServerConnector(ch)))
|
|
if err := ch.Saml.ConfigureSP(request.EmptyContext(s.Log())); err != nil {
|
|
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
|
|
}
|
|
|
|
ch.AddConfigListener(func(_, _ *model.Config) {
|
|
if err := ch.Saml.ConfigureSP(request.EmptyContext(s.Log())); err != nil {
|
|
s.Log().Error("An error occurred while configuring SAML Service Provider", mlog.Err(err))
|
|
}
|
|
})
|
|
}
|
|
|
|
var imgErr error
|
|
decoderConcurrency := int(*ch.cfgSvc.Config().FileSettings.MaxImageDecoderConcurrency)
|
|
if decoderConcurrency == -1 {
|
|
decoderConcurrency = runtime.NumCPU()
|
|
}
|
|
ch.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{
|
|
ConcurrencyLevel: decoderConcurrency,
|
|
})
|
|
if imgErr != nil {
|
|
return nil, errors.Wrap(imgErr, "failed to create image decoder")
|
|
}
|
|
ch.imgEncoder, imgErr = imaging.NewEncoder(imaging.EncoderOptions{
|
|
ConcurrencyLevel: runtime.NumCPU(),
|
|
})
|
|
if imgErr != nil {
|
|
return nil, errors.Wrap(imgErr, "failed to create image encoder")
|
|
}
|
|
|
|
ch.routerSvc = newRouterService()
|
|
services[product.RouterKey] = ch.routerSvc
|
|
|
|
// Setup routes.
|
|
pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
|
|
pluginsRoute.HandleFunc("", ch.ServePluginRequest)
|
|
pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest)
|
|
pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest)
|
|
|
|
services[product.ChannelKey] = &channelsWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.PostKey] = &postServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.PermissionsKey] = &permissionsServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.TeamKey] = &teamServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.BotKey] = &botServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.UserKey] = &App{ch: ch}
|
|
|
|
services[product.PreferencesKey] = &preferencesServiceWrapper{
|
|
app: &App{ch: ch},
|
|
}
|
|
|
|
services[product.CommandKey] = &App{ch: ch}
|
|
|
|
services[product.ThreadsKey] = &App{ch: ch}
|
|
|
|
return ch, nil
|
|
}
|
|
|
|
func (ch *Channels) Start() error {
|
|
// Start plugins
|
|
ctx := request.EmptyContext(ch.srv.Log())
|
|
ch.initPlugins(ctx, *ch.cfgSvc.Config().PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
|
|
|
|
ch.AddConfigListener(func(prevCfg, cfg *model.Config) {
|
|
// We compute the difference between configs
|
|
// to ensure we don't re-init plugins unnecessarily.
|
|
diffs, err := config.Diff(prevCfg, cfg)
|
|
if err != nil {
|
|
ch.srv.Log().Warn("Error in comparing configs", mlog.Err(err))
|
|
return
|
|
}
|
|
|
|
hasDiff := false
|
|
// TODO: This could be a method on ConfigDiffs itself
|
|
for _, diff := range diffs {
|
|
if strings.HasPrefix(diff.Path, "PluginSettings.") {
|
|
hasDiff = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Do only if some plugin related settings has changed.
|
|
if hasDiff {
|
|
if *cfg.PluginSettings.Enable {
|
|
ch.initPlugins(ctx, *cfg.PluginSettings.Directory, *ch.cfgSvc.Config().PluginSettings.ClientDirectory)
|
|
} else {
|
|
ch.ShutDownPlugins()
|
|
}
|
|
}
|
|
})
|
|
|
|
// TODO: This should be moved to the platform service.
|
|
if err := ch.srv.platform.EnsureAsymmetricSigningKey(); err != nil {
|
|
return errors.Wrapf(err, "unable to ensure asymmetric signing key")
|
|
}
|
|
|
|
if err := ch.ensurePostActionCookieSecret(); err != nil {
|
|
return errors.Wrapf(err, "unable to ensure PostAction cookie secret")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *Channels) Stop() error {
|
|
ch.ShutDownPlugins()
|
|
|
|
ch.dndTaskMut.Lock()
|
|
if ch.dndTask != nil {
|
|
ch.dndTask.Cancel()
|
|
}
|
|
ch.dndTaskMut.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *Channels) AddConfigListener(listener func(*model.Config, *model.Config)) string {
|
|
return ch.cfgSvc.AddConfigListener(listener)
|
|
}
|
|
|
|
func (ch *Channels) RemoveConfigListener(id string) {
|
|
ch.cfgSvc.RemoveConfigListener(id)
|
|
}
|
|
|
|
func (ch *Channels) License() *model.License {
|
|
return ch.licenseSvc.GetLicense()
|
|
}
|
|
|
|
func (ch *Channels) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError {
|
|
return ch.licenseSvc.RequestTrialLicenseWithExtraFields(requesterID, trialRequest)
|
|
}
|
|
|
|
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
|
|
return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted,
|
|
receiveEmailsAccepted)
|
|
}
|
|
|
|
func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
|
|
if env := ch.GetPluginsEnvironment(); env != nil {
|
|
env.RunMultiPluginHook(hookRunnerFunc, hookId)
|
|
}
|
|
}
|
|
|
|
func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) {
|
|
var hooks plugin.Hooks
|
|
if env := ch.GetPluginsEnvironment(); env != nil {
|
|
// we intentionally ignore the error here, because the id can be a product id
|
|
// we are going to check if we have the hooks or not
|
|
hooks, _ = env.HooksForPlugin(id)
|
|
if hooks != nil {
|
|
return hooks, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("could not find hooks for id %s", id)
|
|
}
|