From 77dab70ff76eada9fdb089eb416f3e1ec564058c Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Sat, 29 Jul 2023 13:10:50 -0700 Subject: [PATCH] feat(plugin-infra): init permission control (#3461) --- .../bootstrap/plugins/endowments/fercher.ts | 86 ++++++++++++++ .../src/bootstrap/plugins/endowments/timer.ts | 110 ++++++++++++++++++ apps/core/src/bootstrap/plugins/setup.ts | 72 +++++++++--- apps/core/src/bootstrap/register-plugins.ts | 2 +- 4 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 apps/core/src/bootstrap/plugins/endowments/fercher.ts create mode 100644 apps/core/src/bootstrap/plugins/endowments/timer.ts diff --git a/apps/core/src/bootstrap/plugins/endowments/fercher.ts b/apps/core/src/bootstrap/plugins/endowments/fercher.ts new file mode 100644 index 0000000000..eee2d0abf4 --- /dev/null +++ b/apps/core/src/bootstrap/plugins/endowments/fercher.ts @@ -0,0 +1,86 @@ +export interface FetchOptions { + fetch?: typeof fetch; + signal?: AbortSignal; + + normalizeURL?(url: string): string; + + /** + * Virtualize a url + * @param url URL to be rewrite + * @param direction Direction of this rewrite. + * 'in' means the url is from the outside world and should be virtualized. + * 'out' means the url is from the inside world and should be de-virtualized to fetch the real target. + */ + rewriteURL?(url: string, direction: 'in' | 'out'): string; + + replaceRequest?(request: Request): Request | PromiseLike; + + replaceResponse?(response: Response): Response | PromiseLike; + + canConnect?(url: string): boolean | PromiseLike; +} + +export function createFetch(options: FetchOptions) { + const { + fetch: _fetch = fetch, + signal, + rewriteURL, + replaceRequest, + replaceResponse, + canConnect, + normalizeURL, + } = options; + + return async function fetch(input: RequestInfo, init?: RequestInit) { + let request = new Request(input, { + ...init, + signal: getMergedSignal(init?.signal, signal) || null, + }); + + if (normalizeURL) request = new Request(normalizeURL(request.url), request); + if (canConnect && !(await canConnect(request.url))) + throw new TypeError('Failed to fetch'); + if (rewriteURL) + request = new Request(rewriteURL(request.url, 'out'), request); + if (replaceRequest) request = await replaceRequest(request); + + let response = await _fetch(request); + + if (rewriteURL) { + const { url, redirected, type } = response; + // Note: Response constructor does not allow us to set the url of a response. + // we have to define the own property on it. This is not a good simulation. + // To prevent get the original url by Response.prototype.[[get url]].call(response) + // we copy a response and set it's url to empty. + response = new Response(response.body, response); + Object.defineProperties(response, { + url: { value: url, configurable: true }, + redirected: { value: redirected, configurable: true }, + type: { value: type, configurable: true }, + }); + Object.defineProperty(response, 'url', { + configurable: true, + value: rewriteURL(url, 'in'), + }); + } + if (replaceResponse) response = await replaceResponse(response); + return response; + }; +} + +function getMergedSignal( + signal: AbortSignal | undefined | null, + signal2: AbortSignal | undefined | null +) { + if (!signal) return signal2; + if (!signal2) return signal; + + const abortController = new AbortController(); + signal.addEventListener('abort', () => abortController.abort(), { + once: true, + }); + signal2.addEventListener('abort', () => abortController.abort(), { + once: true, + }); + return abortController.signal; +} diff --git a/apps/core/src/bootstrap/plugins/endowments/timer.ts b/apps/core/src/bootstrap/plugins/endowments/timer.ts new file mode 100644 index 0000000000..cd7ea13ef4 --- /dev/null +++ b/apps/core/src/bootstrap/plugins/endowments/timer.ts @@ -0,0 +1,110 @@ +type Handler = (...args: any[]) => void; + +export interface Timers { + setTimeout: (handler: Handler, timeout?: number, ...args: any[]) => number; + clearTimeout: (handle: number) => void; + setInterval: (handler: Handler, timeout?: number, ...args: any[]) => number; + clearInterval: (handle: number) => void; + requestAnimationFrame: (callback: Handler) => number; + cancelAnimationFrame: (handle: number) => void; + requestIdleCallback?: typeof window.requestIdleCallback | undefined; + cancelIdleCallback?: typeof window.cancelIdleCallback | undefined; + queueMicrotask: typeof window.queueMicrotask; +} + +export function createTimers( + abortSignal: AbortSignal, + originalTimes: Timers = { + requestAnimationFrame, + cancelAnimationFrame, + requestIdleCallback: + typeof requestIdleCallback === 'function' + ? requestIdleCallback + : undefined, + cancelIdleCallback: + typeof cancelIdleCallback === 'function' ? cancelIdleCallback : undefined, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + queueMicrotask, + } +): Timers { + const { + requestAnimationFrame: _requestAnimationFrame, + cancelAnimationFrame: _cancelAnimationFrame, + setInterval: _setInterval, + clearInterval: _clearInterval, + setTimeout: _setTimeout, + clearTimeout: _clearTimeout, + cancelIdleCallback: _cancelIdleCallback, + requestIdleCallback: _requestIdleCallback, + queueMicrotask: _queueMicrotask, + } = originalTimes; + + const interval_timer_id: number[] = []; + const idle_id: number[] = []; + const raf_id: number[] = []; + + abortSignal.addEventListener( + 'abort', + () => { + raf_id.forEach(_cancelAnimationFrame); + interval_timer_id.forEach(_clearInterval); + _cancelIdleCallback && idle_id.forEach(_cancelIdleCallback); + }, + { once: true } + ); + + return { + // id is a positive number, it never repeats. + requestAnimationFrame(callback) { + raf_id[raf_id.length] = _requestAnimationFrame(callback); + return raf_id.length; + }, + cancelAnimationFrame(handle) { + const id = raf_id[handle - 1]; + if (!id) return; + _cancelAnimationFrame(id); + }, + setInterval(handler, timeout) { + interval_timer_id[interval_timer_id.length] = (_setInterval as any)( + handler, + timeout + ); + return interval_timer_id.length; + }, + clearInterval(id) { + if (!id) return; + const handle = interval_timer_id[id - 1]; + if (!handle) return; + _clearInterval(handle); + }, + setTimeout(handler, timeout) { + idle_id[idle_id.length] = (_setTimeout as any)(handler, timeout); + return idle_id.length; + }, + clearTimeout(id) { + if (!id) return; + const handle = idle_id[id - 1]; + if (!handle) return; + _clearTimeout(handle); + }, + requestIdleCallback: _requestIdleCallback + ? function requestIdleCallback(callback, options) { + idle_id[idle_id.length] = _requestIdleCallback(callback, options); + return idle_id.length; + } + : undefined, + cancelIdleCallback: _cancelIdleCallback + ? function cancelIdleCallback(handle) { + const id = idle_id[handle - 1]; + if (!id) return; + _cancelIdleCallback(id); + } + : undefined, + queueMicrotask(callback) { + _queueMicrotask(() => abortSignal.aborted || callback()); + }, + }; +} diff --git a/apps/core/src/bootstrap/plugins/setup.ts b/apps/core/src/bootstrap/plugins/setup.ts index 381801bfdf..66bf7f3b3d 100644 --- a/apps/core/src/bootstrap/plugins/setup.ts +++ b/apps/core/src/bootstrap/plugins/setup.ts @@ -1,4 +1,5 @@ import * as AFFiNEComponent from '@affine/component'; +import { DebugLogger } from '@affine/debug'; import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std'; import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils'; import * as Icons from '@blocksuite/icons'; @@ -11,6 +12,11 @@ import * as ReactDom from 'react-dom'; import * as ReactDomClient from 'react-dom/client'; import * as SWR from 'swr'; +import { createFetch } from './endowments/fercher'; +import { createTimers } from './endowments/timer'; + +const logger = new DebugLogger('plugins:permission'); + const setupImportsMap = () => { importsMap.set('react', new Map(Object.entries(React))); importsMap.set('react/jsx-runtime', new Map(Object.entries(ReactJSXRuntime))); @@ -39,27 +45,61 @@ const importsMap = new Map>(); setupImportsMap(); export { importsMap }; -export const createGlobalThis = () => { - return { +const abortController = new AbortController(); + +const pluginFetch = createFetch({}); +const timer = createTimers(abortController.signal); + +const sharedGlobalThis = Object.assign(Object.create(null), timer, { + fetch: pluginFetch, +}); + +export const createGlobalThis = (name: string) => { + return Object.assign(Object.create(null), sharedGlobalThis, { process: Object.freeze({ env: { NODE_ENV: process.env.NODE_ENV, }, }), // UNSAFE: React will read `window` and `document` - window, - document, - navigator, - userAgent: navigator.userAgent, - // todo(himself65): permission control - fetch: function (input: RequestInfo, init?: RequestInit) { - return globalThis.fetch(input, init); - }, - setTimeout: function (callback: () => void, timeout: number) { - return globalThis.setTimeout(callback, timeout); - }, - clearTimeout: function (id: number) { - return globalThis.clearTimeout(id); + window: new Proxy( + {}, + { + get(_, key) { + logger.debug(`${name} is accessing window`, key); + if (sharedGlobalThis[key]) return sharedGlobalThis[key]; + const result = Reflect.get(window, key); + if (typeof result === 'function') { + return function (...args: any[]) { + logger.debug(`${name} is calling window`, key, args); + return result.apply(window, args); + }; + } + logger.debug('window', key, result); + return result; + }, + } + ), + document: new Proxy( + {}, + { + get(_, key) { + logger.debug(`${name} is accessing document`, key); + if (sharedGlobalThis[key]) return sharedGlobalThis[key]; + const result = Reflect.get(document, key); + if (typeof result === 'function') { + return function (...args: any[]) { + logger.debug(`${name} is calling window`, key, args); + return result.apply(document, args); + }; + } + logger.debug('document', key, result); + return result; + }, + } + ), + navigator: { + userAgent: navigator.userAgent, }, // safe to use for all plugins @@ -97,5 +137,5 @@ export const createGlobalThis = () => { IDBIndex: globalThis.IDBIndex, IDBCursor: globalThis.IDBCursor, IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent, - }; + }); }; diff --git a/apps/core/src/bootstrap/register-plugins.ts b/apps/core/src/bootstrap/register-plugins.ts index da04b28d64..31cf9e3fc4 100644 --- a/apps/core/src/bootstrap/register-plugins.ts +++ b/apps/core/src/bootstrap/register-plugins.ts @@ -102,7 +102,7 @@ await Promise.all( if (!release && process.env.NODE_ENV === 'production') { return Promise.resolve(); } - const pluginCompartment = new Compartment(createGlobalThis()); + const pluginCompartment = new Compartment(createGlobalThis(pluginName)); const baseURL = url; const entryURL = `${baseURL}/${core}`; rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);