diff --git a/.betterer.results b/.betterer.results index 08682acb6a7..6359886fbbe 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2863,6 +2863,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Do not use any type assertions.", "8"] ], + "public/app/features/plugins/sandbox/distortion_map.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/plugins/sandbox/sandbox_components.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/plugins/sandbox/distortion_map.ts b/public/app/features/plugins/sandbox/distortion_map.ts index 56ec93f8058..ad8d2bacfc4 100644 --- a/public/app/features/plugins/sandbox/distortion_map.ts +++ b/public/app/features/plugins/sandbox/distortion_map.ts @@ -2,11 +2,12 @@ import { cloneDeep, isFunction } from 'lodash'; import { PluginMeta } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { Monaco } from '@grafana/ui'; import { loadScriptIntoSandbox } from './code_loader'; import { forbiddenElements } from './constants'; import { SandboxEnvironment } from './types'; -import { logWarning } from './utils'; +import { logWarning, unboxRegexesFromMembraneProxy } from './utils'; /** * Distortions are near-membrane mechanisms to altert JS instrics and DOM APIs. @@ -67,6 +68,8 @@ const generalDistortionMap: DistortionMap = new Map(); const monitorOnly = Boolean(config.featureToggles.frontendSandboxMonitorOnly); +const SANDBOX_LIVE_API_PATCHED = Symbol.for('@SANDBOX_LIVE_API_PATCHED'); + export function getGeneralSandboxDistortionMap() { if (generalDistortionMap.size === 0) { // initialize the distortion map @@ -79,6 +82,7 @@ export function getGeneralSandboxDistortionMap() { distortCreateElement(generalDistortionMap); distortWorkers(generalDistortionMap); distortDocument(generalDistortionMap); + distortMonacoEditor(generalDistortionMap); } return generalDistortionMap; } @@ -456,3 +460,48 @@ function distortDocument(distortions: DistortionMap) { } } } + +async function distortMonacoEditor(distortions: DistortionMap) { + // We rely on `monaco` being instanciated inside `window.monaco`. + // this is the same object passed down to plugins using monaco editor for their editors + // this `window.monaco` is an instance of monaco but not the same as if we + // import `monaco-editor` directly in this file. + // Short of abusing the `window.monaco` object we would have to modify grafana-ui to export + // the monaco instance directly in the ReactMonacoEditor component + const monacoEditor: Monaco = Reflect.get(window, 'monaco'); + + // do not double patch + if (!monacoEditor || Object.hasOwn(monacoEditor, SANDBOX_LIVE_API_PATCHED)) { + return; + } + const originalSetMonarchTokensProvider = monacoEditor.languages.setMonarchTokensProvider; + + // NOTE: this function in particular is called only once per intialized custom language inside a plugin which is a + // rare ocurrance but if not patched it'll break the syntax highlighting for the custom language. + function getSetMonarchTokensProvider() { + return function (...args: Parameters) { + if (args.length !== 2) { + return originalSetMonarchTokensProvider.apply(monacoEditor, args); + } + return originalSetMonarchTokensProvider.call( + monacoEditor, + args[0], + unboxRegexesFromMembraneProxy(args[1]) as (typeof args)[1] + ); + }; + } + distortions.set(monacoEditor.languages.setMonarchTokensProvider, getSetMonarchTokensProvider); + Reflect.set(monacoEditor, SANDBOX_LIVE_API_PATCHED, {}); +} + +/** + * We define "live" APIs as APIs that can only be distorted in runtime on-the-fly and not at initialization + * time like other distortions do. + * + * This could be because the objects we want to patch only become available after specific states are reached + * or because the libraries we want to patch are lazy-loaded and we don't have access to their definitions + * + */ +export async function distortLiveApis() { + distortMonacoEditor(generalDistortionMap); +} diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts index a779a3a8d70..42bec39b023 100644 --- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts +++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts @@ -6,7 +6,7 @@ import { PluginMeta } from '@grafana/data'; import { getPluginSettings } from '../pluginSettings'; import { getPluginCode } from './code_loader'; -import { getGeneralSandboxDistortionMap } from './distortion_map'; +import { getGeneralSandboxDistortionMap, distortLiveApis } from './distortion_map'; import { getSafeSandboxDomElement, isDomElement, @@ -60,6 +60,7 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise } else { patchObjectAsLiveTarget(originalValue); } + distortLiveApis(); const distortion = generalDistortionMap.get(originalValue); if (distortion) { return distortion(originalValue, meta, sandboxEnvironment) as ProxyTarget; diff --git a/public/app/features/plugins/sandbox/utils.ts b/public/app/features/plugins/sandbox/utils.ts index 516d7d28739..fccee702128 100644 --- a/public/app/features/plugins/sandbox/utils.ts +++ b/public/app/features/plugins/sandbox/utils.ts @@ -1,3 +1,4 @@ +import { isNearMembraneProxy } from '@locker/near-membrane-shared'; import React from 'react'; import { LogContext } from '@grafana/faro-web-sdk'; @@ -36,3 +37,36 @@ export function logError(error: Error, context?: LogContext) { }; logErrorRuntime(error, context); } + +function isRegex(value: unknown): value is RegExp { + return value?.constructor?.name === 'RegExp'; +} + +/** + * Near membrane regex proxy objects behave just exactly the same as regular RegExp objects + * with only one difference: they are not `instanceof RegExp`. + * This function takes a structure and makes sure any regex that is a nearmembraneproxy + * and returns the same regex but in the bluerealm + */ +export function unboxRegexesFromMembraneProxy(structure: unknown): unknown { + if (!structure) { + return structure; + } + + // Proxy regexes loook and behave like proxies but they + // are not instanceof RegExp + if (isRegex(structure) && isNearMembraneProxy(structure)) { + return new RegExp(structure); + } + + if (Array.isArray(structure)) { + return structure.map(unboxRegexesFromMembraneProxy); + } + if (typeof structure === 'object') { + return Object.keys(structure).reduce((acc, key) => { + Reflect.set(acc, key, unboxRegexesFromMembraneProxy(Reflect.get(structure, key))); + return acc; + }, {}); + } + return structure; +}