2023-10-03 09:02:55 +02:00
|
|
|
import { PluginMeta, patchArrayVectorProrotypeMethods } from '@grafana/data';
|
2023-07-20 13:19:57 +02:00
|
|
|
|
2023-08-31 15:45:44 +02:00
|
|
|
import { transformPluginSourceForCDN } from '../cdn/utils';
|
2023-09-27 13:56:35 +02:00
|
|
|
import { resolveWithCache } from '../loader/cache';
|
2023-08-31 15:45:44 +02:00
|
|
|
import { isHostedOnCDN } from '../loader/utils';
|
2023-07-20 13:19:57 +02:00
|
|
|
|
|
|
|
|
import { SandboxEnvironment } from './types';
|
|
|
|
|
|
|
|
|
|
function isSameDomainAsHost(url: string): boolean {
|
|
|
|
|
const locationUrl = new URL(window.location.href);
|
|
|
|
|
const paramUrl = new URL(url);
|
|
|
|
|
return locationUrl.host === paramUrl.host;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function loadScriptIntoSandbox(url: string, meta: PluginMeta, sandboxEnv: SandboxEnvironment) {
|
|
|
|
|
let scriptCode = '';
|
|
|
|
|
|
|
|
|
|
// same-domain
|
|
|
|
|
if (isSameDomainAsHost(url)) {
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
scriptCode = await response.text();
|
|
|
|
|
scriptCode = patchPluginSourceMap(meta, scriptCode);
|
|
|
|
|
|
|
|
|
|
// cdn loaded
|
2023-08-31 15:45:44 +02:00
|
|
|
} else if (isHostedOnCDN(url)) {
|
2023-07-20 13:19:57 +02:00
|
|
|
const response = await fetch(url);
|
|
|
|
|
scriptCode = await response.text();
|
|
|
|
|
scriptCode = transformPluginSourceForCDN({
|
2023-08-31 15:45:44 +02:00
|
|
|
url,
|
2023-07-20 13:19:57 +02:00
|
|
|
source: scriptCode,
|
2023-08-31 15:45:44 +02:00
|
|
|
transformSourceMapURL: true,
|
2023-07-20 13:19:57 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scriptCode.length === 0) {
|
|
|
|
|
throw new Error('Only same domain scripts are allowed in sandboxed plugins');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sandboxEnv.evaluate(scriptCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getPluginCode(meta: PluginMeta): Promise<string> {
|
2023-08-31 15:45:44 +02:00
|
|
|
if (isHostedOnCDN(meta.module)) {
|
2023-09-27 13:56:35 +02:00
|
|
|
// Load plugin from CDN, no need for "resolveWithCache" as CDN URLs already include the version
|
2023-08-31 15:45:44 +02:00
|
|
|
const url = meta.module;
|
|
|
|
|
const response = await fetch(url);
|
2023-07-20 13:19:57 +02:00
|
|
|
let pluginCode = await response.text();
|
|
|
|
|
pluginCode = transformPluginSourceForCDN({
|
2023-08-31 15:45:44 +02:00
|
|
|
url,
|
2023-07-20 13:19:57 +02:00
|
|
|
source: pluginCode,
|
2023-08-31 15:45:44 +02:00
|
|
|
transformSourceMapURL: true,
|
2023-07-20 13:19:57 +02:00
|
|
|
});
|
|
|
|
|
return pluginCode;
|
|
|
|
|
} else {
|
2023-09-27 13:56:35 +02:00
|
|
|
// local plugin. resolveWithCache will append a query parameter with its version
|
|
|
|
|
// to ensure correct cached version is served
|
|
|
|
|
const pluginCodeUrl = resolveWithCache(meta.module);
|
|
|
|
|
const response = await fetch(pluginCodeUrl);
|
2023-07-20 13:19:57 +02:00
|
|
|
let pluginCode = await response.text();
|
|
|
|
|
pluginCode = patchPluginSourceMap(meta, pluginCode);
|
2023-07-28 14:04:23 +02:00
|
|
|
pluginCode = patchPluginAPIs(pluginCode);
|
2023-07-20 13:19:57 +02:00
|
|
|
return pluginCode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-28 14:04:23 +02:00
|
|
|
function patchPluginAPIs(pluginCode: string): string {
|
|
|
|
|
return pluginCode.replace(/window\.location/gi, 'window.locationSandbox');
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-20 13:19:57 +02:00
|
|
|
/**
|
|
|
|
|
* Patches the plugin's module.js source code references to sourcemaps to include the full url
|
|
|
|
|
* of the module.js file instead of the regular relative reference.
|
|
|
|
|
*
|
|
|
|
|
* Because the plugin module.js code is loaded via fetch and then "eval" as a string
|
|
|
|
|
* it can't find the references to the module.js.map directly and we need to patch it
|
|
|
|
|
* to point to the correct location
|
|
|
|
|
*/
|
|
|
|
|
function patchPluginSourceMap(meta: PluginMeta, pluginCode: string): string {
|
|
|
|
|
// skips inlined and files without source maps
|
|
|
|
|
if (pluginCode.includes('//# sourceMappingURL=module.js.map')) {
|
|
|
|
|
let replaceWith = '';
|
|
|
|
|
// make sure we don't add the sourceURL twice
|
|
|
|
|
if (!pluginCode.includes('//# sourceURL') || !pluginCode.includes('//@ sourceUrl')) {
|
|
|
|
|
replaceWith += `//# sourceURL=module.js\n`;
|
|
|
|
|
}
|
|
|
|
|
// modify the source map url to point to the correct location
|
2023-09-06 12:49:09 +02:00
|
|
|
const sourceCodeMapUrl = meta.module + '.map';
|
2023-07-20 13:19:57 +02:00
|
|
|
replaceWith += `//# sourceMappingURL=${sourceCodeMapUrl}`;
|
|
|
|
|
|
|
|
|
|
return pluginCode.replace('//# sourceMappingURL=module.js.map', replaceWith);
|
|
|
|
|
}
|
|
|
|
|
return pluginCode;
|
|
|
|
|
}
|
2023-10-03 09:02:55 +02:00
|
|
|
|
|
|
|
|
export function patchSandboxEnvironmentPrototype(sandboxEnvironment: SandboxEnvironment) {
|
|
|
|
|
// same as https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/vector.ts#L16
|
|
|
|
|
// Array is a "reflective" type in Near-membrane and doesn't get an identify continuity
|
|
|
|
|
sandboxEnvironment.evaluate(
|
|
|
|
|
`${patchArrayVectorProrotypeMethods.toString()};${patchArrayVectorProrotypeMethods.name}()`
|
|
|
|
|
);
|
|
|
|
|
}
|