From c3c81cb3d6a29ef8164a8e7ef4ba172d60117176 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 6 Dec 2022 12:44:48 -0500 Subject: [PATCH] MM-48120/MM-48623 Patch plugin bundle react-dom on webapp extract (#21171) Automatic Merge --- app/plugin.go | 10 +++- app/plugin_api_test.go | 12 ++--- app/plugin_hooks_test.go | 4 +- model/config.go | 5 ++ plugin/environment.go | 71 ++++++++++++++++++++++++++-- services/telemetry/telemetry.go | 1 + services/telemetry/telemetry_test.go | 1 + web/web_test.go | 2 +- 8 files changed, 93 insertions(+), 13 deletions(-) diff --git a/app/plugin.go b/app/plugin.go index 93d47fd5b6..7ce6c47fe6 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -243,7 +243,15 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s return New(ServerConnector(ch)).NewPluginAPI(c, manifest) } - env, err := plugin.NewEnvironment(newAPIFunc, NewDriverImpl(ch.srv), pluginDir, webappPluginDir, ch.srv.Log(), ch.srv.GetMetrics()) + env, err := plugin.NewEnvironment( + newAPIFunc, + NewDriverImpl(ch.srv), + pluginDir, + webappPluginDir, + *ch.cfgSvc.Config().ExperimentalSettings.PatchPluginsReactDOM, + ch.srv.Log(), + ch.srv.GetMetrics(), + ) if err != nil { mlog.Error("Failed to start up plugins", mlog.Err(err)) return diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 9f864109bf..759613781b 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -92,7 +92,7 @@ func setupMultiPluginAPITest(t *testing.T, pluginCodes []string, pluginManifests return app.NewPluginAPI(c, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) require.Equal(t, len(pluginCodes), len(pluginIDs)) @@ -849,7 +849,7 @@ func TestPluginAPIGetPlugins(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), nil) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) pluginIDs := []string{"pluginid1", "pluginid2", "pluginid3"} @@ -937,7 +937,7 @@ func TestInstallPlugin(t *testing.T) { return app.NewPluginAPI(c, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) app.ch.SetPluginsEnvironment(env) @@ -1632,7 +1632,7 @@ func TestAPIMetrics(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), metricsMock) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) require.NoError(t, err) th.App.ch.SetPluginsEnvironment(env) @@ -2079,7 +2079,7 @@ func TestRegisterCollectionAndTopic(t *testing.T) { return th.App.NewPluginAPI(th.Context, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, th.App.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) th.App.ch.SetPluginsEnvironment(env) @@ -2179,7 +2179,7 @@ func TestPluginUploadsAPI(t *testing.T) { newPluginAPI := func(manifest *model.Manifest) plugin.API { return th.App.NewPluginAPI(th.Context, manifest) } - env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, th.App.Log(), nil) + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) th.App.ch.SetPluginsEnvironment(env) diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index 7cd81f8fae..161994a1ab 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -33,7 +33,7 @@ func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, a webappPluginDir, err := os.MkdirTemp("", "") require.NoError(t, err) - env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil) + env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil) require.NoError(t, err) app.ch.SetPluginsEnvironment(env) @@ -1030,7 +1030,7 @@ func TestHookMetrics(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), metricsMock) + env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock) require.NoError(t, err) th.App.ch.SetPluginsEnvironment(env) diff --git a/model/config.go b/model/config.go index 058a7b7666..dd0ad5cfd3 100644 --- a/model/config.go +++ b/model/config.go @@ -968,6 +968,7 @@ type ExperimentalSettings struct { EnableSharedChannels *bool `access:"experimental_features"` EnableRemoteClusterService *bool `access:"experimental_features"` EnableAppBar *bool `access:"experimental_features"` + PatchPluginsReactDOM *bool `access:"experimental_features"` } func (s *ExperimentalSettings) SetDefaults() { @@ -1002,6 +1003,10 @@ func (s *ExperimentalSettings) SetDefaults() { if s.EnableAppBar == nil { s.EnableAppBar = NewBool(false) } + + if s.PatchPluginsReactDOM == nil { + s.PatchPluginsReactDOM = NewBool(false) + } } type AnalyticsSettings struct { diff --git a/plugin/environment.go b/plugin/environment.go index db07f051bd..4ccbbdf023 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -4,10 +4,12 @@ package plugin import ( + "bytes" "fmt" "hash/fnv" "os" "path/filepath" + "strings" "sync" "time" @@ -57,15 +59,20 @@ type Environment struct { dbDriver Driver pluginDir string webappPluginDir string + patchReactDOM bool prepackagedPlugins []*PrepackagedPlugin prepackagedPluginsLock sync.RWMutex } -func NewEnvironment(newAPIImpl apiImplCreatorFunc, +func NewEnvironment( + newAPIImpl apiImplCreatorFunc, dbDriver Driver, - pluginDir string, webappPluginDir string, + pluginDir string, + webappPluginDir string, + patchReactDOM bool, logger *mlog.Logger, - metrics einterfaces.MetricsInterface) (*Environment, error) { + metrics einterfaces.MetricsInterface, +) (*Environment, error) { return &Environment{ logger: logger, metrics: metrics, @@ -73,6 +80,7 @@ func NewEnvironment(newAPIImpl apiImplCreatorFunc, dbDriver: dbDriver, pluginDir: pluginDir, webappPluginDir: webappPluginDir, + patchReactDOM: patchReactDOM, }, nil } @@ -451,6 +459,17 @@ func (env *Environment) UnpackWebappBundle(id string) (*model.Manifest, error) { return nil, errors.Wrapf(err, "unable to read webapp bundle: %v", id) } + if env.patchReactDOM { + newContents, changed := patchReactDOM(sourceBundleFileContents) + if changed { + sourceBundleFileContents = newContents + err = os.WriteFile(sourceBundleFilepath, sourceBundleFileContents, 0644) + if err != nil { + return nil, errors.Wrapf(err, "unable to overwrite webapp bundle: %v", id) + } + } + } + hash := fnv.New64a() if _, err = hash.Write(sourceBundleFileContents); err != nil { return nil, errors.Wrapf(err, "unable to generate hash for webapp bundle: %v", id) @@ -467,6 +486,52 @@ func (env *Environment) UnpackWebappBundle(id string) (*model.Manifest, error) { return manifest, nil } +func patchReactDOM(initialBytes []byte) ([]byte, bool) { + if !bytes.Contains(initialBytes, []byte("react-dom.production.min.js")) { + return initialBytes, false + } + + initial := string(initialBytes) + nameIndex := strings.Index(initial, "react-dom.production.min.js") + + beginning := strings.LastIndex(initial[:nameIndex], "{") + var end int + + argDefBeginning := strings.LastIndex(initial[:beginning], "function") + 9 + argDefEnd := strings.LastIndex(initial[:beginning], ")") - 1 + argsNames := strings.Split(initial[argDefBeginning:argDefEnd], ",") + if len(argsNames) != 3 { + return initialBytes, false + } + + exportsArgName := strings.TrimSpace(argsNames[1]) + + numOpenBraces := 0 + for i, c := range initial[beginning:] { + if end != 0 { + break + } + switch c { + case '}': + numOpenBraces-- + + if numOpenBraces == 0 { + end = beginning + i + } + case '{': + numOpenBraces++ + } + } + + beforePatch := initial[:end] + afterPatch := initial[end:] + + patch := fmt.Sprintf("; Object.assign(%s, window.ReactDOM)", exportsArgName) + + result := fmt.Sprintf("%s%s%s", beforePatch, patch, afterPatch) + return []byte(result), true +} + // HooksForPlugin returns the hooks API for the plugin with the given id. // // Consider using RunMultiPluginHook instead. diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index ee8fea6f11..ce9eb79419 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -734,6 +734,7 @@ func (ts *TelemetryService) trackConfig() { "enable_shared_channels": *cfg.ExperimentalSettings.EnableSharedChannels, "enable_remote_cluster_service": *cfg.ExperimentalSettings.EnableRemoteClusterService && cfg.FeatureFlags.EnableRemoteClusterService, "enable_app_bar": *cfg.ExperimentalSettings.EnableAppBar, + "patch_plugins_react_dom": *cfg.ExperimentalSettings.PatchPluginsReactDOM, }) ts.SendTelemetry(TrackConfigAnalytics, map[string]any{ diff --git a/services/telemetry/telemetry_test.go b/services/telemetry/telemetry_test.go index 31ab7656ad..32a86602a3 100644 --- a/services/telemetry/telemetry_test.go +++ b/services/telemetry/telemetry_test.go @@ -165,6 +165,7 @@ func initializeMocks(cfg *model.Config, cloudLicense bool) (*mocks.ServerIface, func(m *model.Manifest) plugin.API { return pluginsAPIMock }, nil, pluginDir, webappPluginDir, + false, logger, nil) serverIfaceMock.On("GetPluginsEnvironment").Return(pluginEnv, nil) diff --git a/web/web_test.go b/web/web_test.go index 139753675d..ae8e322f91 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -280,7 +280,7 @@ func TestPublicFilesRequest(t *testing.T) { defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) - env, err := plugin.NewEnvironment(th.NewPluginAPI, app.NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), nil) + env, err := plugin.NewEnvironment(th.NewPluginAPI, app.NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), nil) require.NoError(t, err) pluginID := "com.mattermost.sample"