From 7107ba01048057088cbc3153524e071c5f31f4a9 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 7 Nov 2023 09:09:17 +0100 Subject: [PATCH] Sandbox: Patch history.replaceState to make it work inside the sandbox (#76255) * Patch history.replaceState api instead of doing a live distortion * Add better patching mechanism * Remove console log --- .../plugins/sandbox/distortion_map.ts | 19 -------- .../plugins/sandbox/document_sandbox.ts | 48 +++++++++++++++++++ .../plugins/sandbox/sandbox_plugin_loader.ts | 2 + 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/public/app/features/plugins/sandbox/distortion_map.ts b/public/app/features/plugins/sandbox/distortion_map.ts index eeb6fd756ff..11b4392a37a 100644 --- a/public/app/features/plugins/sandbox/distortion_map.ts +++ b/public/app/features/plugins/sandbox/distortion_map.ts @@ -528,24 +528,5 @@ async function distortPostMessage(distortions: DistortionMap) { */ export function distortLiveApis(originalValue: ProxyTarget): ProxyTarget | undefined { distortMonacoEditor(generalDistortionMap); - - // This distorts the `history.replace` function in react-router-dom. - // constructed for each browser history and is only accessible within the react context. - // Note that this distortion does not affect `String.prototype.replace` calls. - // because they don't go through distortions - if ( - originalValue instanceof Function && - originalValue.name === 'replace' && - originalValue.prototype.constructor.length === 2 - ) { - return function replace(this: unknown, ...args: unknown[]) { - // validate history.replace signature further - if (args && args[0] && typeof args[0] === 'string' && args[1] && !(args[1] instanceof Function)) { - const newArgs = cloneDeep(args); - return Reflect.apply(originalValue, this, newArgs); - } - return Reflect.apply(originalValue, this, args); - }; - } return; } diff --git a/public/app/features/plugins/sandbox/document_sandbox.ts b/public/app/features/plugins/sandbox/document_sandbox.ts index 75199726c5c..6a61407aded 100644 --- a/public/app/features/plugins/sandbox/document_sandbox.ts +++ b/public/app/features/plugins/sandbox/document_sandbox.ts @@ -1,4 +1,5 @@ import { isNearMembraneProxy, ProxyTarget } from '@locker/near-membrane-shared'; +import { cloneDeep } from 'lodash'; import Prism from 'prismjs'; import { DataSourceApi } from '@grafana/data'; @@ -160,3 +161,50 @@ export function getSandboxMockBody(): Element { } return sandboxBody; } + +let nativeAPIsPatched = false; + +export function patchWebAPIs() { + if (!nativeAPIsPatched) { + nativeAPIsPatched = true; + patchHistoryReplaceState(); + } +} + +/* + * window.history.replaceState is a native API that won't work with proxies + * so we need to patch it to unwrap any possible proxies you pass to it. + * + * Why can't we directly distord window.history.replaceState calls inside plugins? + * + * We can. Except that plugins don't call window.history.replaceState directly they + * instead use the history object from react-router. + * + * react-router is a runtime dependency and it is executed in the blue realm + * and calls window.history.replaceState directly where the sandbox is not involved at all + * + * It is most likely this "original" function is not really the native function because + * `useLocation` from `react-use` patches this function before the sandbox kicks in. + * + * Regarding the performance impact of this cloneDeep. The structures passed to history.replaceState + * are minimalistic and its impact will be neglegible. + */ +function patchHistoryReplaceState() { + const original = window.history.replaceState; + Object.defineProperty(window.history, 'replaceState', { + value: function (...args: Parameters) { + let newArgs = args; + try { + newArgs = cloneDeep(args); + } catch (e) { + logWarning('Error cloning args in window.history.replaceState', { + error: String(e), + }); + } + return Reflect.apply(original, this, newArgs); + }, + writable: true, + configurable: true, + enumerable: false, + }); +} diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts index c6884d816b6..02e4be6d8b3 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -15,6 +15,7 @@ import { isLiveTarget, markDomElementStyleAsALiveTarget, patchObjectAsLiveTarget, + patchWebAPIs, } from './document_sandbox'; import { sandboxPluginDependencies } from './plugin_dependencies'; import { sandboxPluginComponents } from './sandbox_components'; @@ -30,6 +31,7 @@ const pluginImportCache = new Map>(); const pluginLogCache: Record = {}; export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise { + patchWebAPIs(); try { const pluginMeta = await getPluginSettings(pluginId); if (!pluginImportCache.has(pluginId)) {