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
This commit is contained in:
Esteban Beltran 2023-11-07 09:09:17 +01:00 committed by GitHub
parent 63c7a0e8ca
commit 7107ba0104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 50 additions and 19 deletions

View File

@ -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;
}

View File

@ -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<typeof window.history.replaceState>) {
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,
});
}

View File

@ -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<string, Promise<System.Module>>();
const pluginLogCache: Record<string, boolean> = {};
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<System.Module> {
patchWebAPIs();
try {
const pluginMeta = await getPluginSettings(pluginId);
if (!pluginImportCache.has(pluginId)) {