Sandbox: Fix monaco editor custom languages not working correctly inside the sandbox (#72911)

This commit is contained in:
Esteban Beltran 2023-08-11 10:50:57 +02:00 committed by GitHub
parent 51a67b99f2
commit 84181eb613
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 2 deletions

View File

@ -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"]
],

View File

@ -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<typeof originalSetMonarchTokensProvider>) {
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);
}

View File

@ -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<unknown>
} else {
patchObjectAsLiveTarget(originalValue);
}
distortLiveApis();
const distortion = generalDistortionMap.get(originalValue);
if (distortion) {
return distortion(originalValue, meta, sandboxEnvironment) as ProxyTarget;

View File

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