From bcd14ddebbb17aeb064109758b717549cef8fdf5 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 26 Mar 2021 17:06:23 +0800 Subject: [PATCH 01/28] feat: add basic parse of external plugins --- .../src/plugins/External/Container.tsx | 43 +++++++++++++ .../maskbook/src/plugins/External/README.md | 17 +++++ .../components/ExternalPluginRenderer.tsx | 46 ++++++++++++++ .../components/UnknownPluginLoadRequest.tsx | 63 +++++++++++++++++++ .../maskbook/src/plugins/External/define.tsx | 33 ++++++++++ .../maskbook/src/plugins/External/types.tsx | 15 +++++ packages/maskbook/src/plugins/PluginUI.ts | 2 + 7 files changed, 219 insertions(+) create mode 100644 packages/maskbook/src/plugins/External/Container.tsx create mode 100644 packages/maskbook/src/plugins/External/README.md create mode 100644 packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx create mode 100644 packages/maskbook/src/plugins/External/components/UnknownPluginLoadRequest.tsx create mode 100644 packages/maskbook/src/plugins/External/define.tsx create mode 100644 packages/maskbook/src/plugins/External/types.tsx diff --git a/packages/maskbook/src/plugins/External/Container.tsx b/packages/maskbook/src/plugins/External/Container.tsx new file mode 100644 index 000000000000..b8471c27990a --- /dev/null +++ b/packages/maskbook/src/plugins/External/Container.tsx @@ -0,0 +1,43 @@ +import { SnackbarContent } from '@material-ui/core' +import { useAsyncRetry } from 'react-use' +import { Suspense, useRef } from 'react' +import type { ExternalPluginLoadDetails } from './types' +import { UnknownPluginLoadRequestUI } from './components/UnknownPluginLoadRequest' +import { ExternalPluginRenderer } from './components/ExternalPluginRenderer' +export interface ExternalPluginContainerProps { + plugins: ExternalPluginLoadDetails[] +} +export function ExternalPluginLoader(props: ExternalPluginContainerProps) { + // TODO: this section should use suspense and it will more nature + const { loading, value, retry } = useAsyncRetry(() => filterPlugin(props.plugins), [props.plugins.join('@')]) + const ref = useRef(value) + if (!ref.current && loading) return null + ref.current = value + if (!ref.current) return null + // + const { known, unknown } = ref.current + return ( + <> + { + list.forEach((x) => allowed.add(x.url)) + retry() + }} + /> + {known.map((x) => ( + }> + + + ))} + + ) +} +const allowed = new Set() +/** This function should query which plugin can be loaded directly. */ +async function filterPlugin(plugins: ExternalPluginLoadDetails[]) { + return { + unknown: plugins.filter((x) => !allowed.has(x.url)), + known: plugins.filter((x) => allowed.has(x.url)), + } +} diff --git a/packages/maskbook/src/plugins/External/README.md b/packages/maskbook/src/plugins/External/README.md new file mode 100644 index 000000000000..4014bf9da9a9 --- /dev/null +++ b/packages/maskbook/src/plugins/External/README.md @@ -0,0 +1,17 @@ +# External plugins + +## Plugin management (TBD) + +Priority (highest to lowest): + +- Precise (`example.com/plugin`) +- Publisher (Must be code signed) (`publisher=HASH_OF_PUBLIC_KEY`) +- Domain based (`*.example.com/*`) +- Fallback/default settings + +### Settings can be tweaked (TBD) + +- Permission (only applicable for `Precise`, must grant case-by-case) +- Auto load unknown plugins (Not applicable for `Publisher`) +- Auto update plugins periodically +- Auto update everytime I use it (might cause too many web requests) diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx new file mode 100644 index 000000000000..4c5dc0c96dd1 --- /dev/null +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -0,0 +1,46 @@ +import type { ExternalPluginLoadDetails, Manifest } from '../types' +import { Card, CardHeader, CardContent, Typography, CardActions, Link } from '@material-ui/core' +import { useAsync } from 'react-use' +import Services from '../../../extension/service' +export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { + const manifest = useExternalPluginManifest(props.url) + if (!manifest) return null + return ( + + + + Publisher: {manifest.publisher} (⚠ Unverified) +
+ Plugin URL: {props.url} +
+ + } + /> + Plugin render area + +
+ ) +} + +// TODO: support suspense +function useExternalPluginManifest(url: string): Manifest | undefined { + const { value } = useAsync(() => fetchManifest(url), [url]) + return value +} + +async function fetchManifest(addr: string) { + const blob = await Services.Helper.fetch(addr + 'mask-manifest.json') + const json = await blob.text().then(JSONC) + // TODO: verify manifest + return JSON.parse(json) +} + +function JSONC(x: string) { + return x + .split('\n') + .filter((x) => !x.match(/^ +\/\//)) + .join('\n') +} diff --git a/packages/maskbook/src/plugins/External/components/UnknownPluginLoadRequest.tsx b/packages/maskbook/src/plugins/External/components/UnknownPluginLoadRequest.tsx new file mode 100644 index 000000000000..c96d05eefd22 --- /dev/null +++ b/packages/maskbook/src/plugins/External/components/UnknownPluginLoadRequest.tsx @@ -0,0 +1,63 @@ +import { + SnackbarContent, + Button, + List, + Checkbox, + ListSubheader, + ListItem, + ListItemText, + ListItemIcon, + Card, + CardContent, + CardActions, +} from '@material-ui/core' +import { useMap } from 'react-use' +import type { ExternalPluginLoadDetails } from '../types' + +export function UnknownPluginLoadRequestUI({ plugins, onConfirm }: UnknownPluginLoadRequestProps) { + const [_selected, { get, set }] = useMap({} as Record) + + const confirmAll = () => onConfirm(plugins) + const selected = plugins.filter((x) => _selected[x.url]) + const confirmSelected = () => onConfirm(selected) + + if (plugins.length === 0) return null + if (plugins.length === 1) + return ( + Load} + /> + ) + return ( + + + New unknown Mask plugins found. Do you want to load them? + }> + {plugins.map((x) => ( + set(x.url, !get(x.url))} key={x.url}> + + + + + + ))} + + + + + + + + ) +} + +export interface UnknownPluginLoadRequestProps { + plugins: ExternalPluginLoadDetails[] + onConfirm(list: ExternalPluginLoadDetails[]): void +} diff --git a/packages/maskbook/src/plugins/External/define.tsx b/packages/maskbook/src/plugins/External/define.tsx new file mode 100644 index 000000000000..f31227168cc0 --- /dev/null +++ b/packages/maskbook/src/plugins/External/define.tsx @@ -0,0 +1,33 @@ +import { Suspense } from 'react' +import { PluginConfig, PluginScope, PluginStage } from '../types' +import { ExternalPluginLoader } from './Container' +import type { ExternalPluginLoadDetails } from './types' +export const ExternalPluginDefine: PluginConfig = { + identifier: 'com.maskbook.external', + pluginName: 'External Plugin', + scope: PluginScope.Public, + stage: PluginStage.Production, + successDecryptionInspector: function Comp(props) { + const tm = props.message + if (!tm.meta) return null + let JSX: ExternalPluginLoadDetails[] = [] + for (const [key, meta] of tm.meta) { + if (!key.startsWith('plugin:')) continue + const [url, metaKey] = parse(key) + JSX.push({ meta, metaKey, url }) + } + // Loader itself has some async work and no need to display loading fallback + return ( + + + + ) + }, +} +// plugin:dimensiondev.github.io/Mask-Plugin-Example/@v1 +function parse(x: string) { + let [address, ...key] = x.slice('plugin:'.length).split('@') + if (!address.endsWith('/')) address += '/' + const isLocalhost = new URL('https://' + address).hostname + return [(isLocalhost ? 'http://' : 'https://') + address, key.join('@')] +} diff --git a/packages/maskbook/src/plugins/External/types.tsx b/packages/maskbook/src/plugins/External/types.tsx new file mode 100644 index 000000000000..6a2c00d8e9d6 --- /dev/null +++ b/packages/maskbook/src/plugins/External/types.tsx @@ -0,0 +1,15 @@ +export interface ExternalPluginLoadDetails { + /** Where is the plugin hosts */ + url: string + /** Stripped meta key that invokes this plugin */ + metaKey: string + /** The metadata content */ + meta: unknown +} +export interface Manifest { + manifest_version: 0 + name: string + description: string + publisher: string + metadata: Record +} diff --git a/packages/maskbook/src/plugins/PluginUI.ts b/packages/maskbook/src/plugins/PluginUI.ts index 998a13fb5493..f16d037432ad 100644 --- a/packages/maskbook/src/plugins/PluginUI.ts +++ b/packages/maskbook/src/plugins/PluginUI.ts @@ -4,6 +4,7 @@ import type { PluginConfig } from './types' const plugins = new Set() export const PluginUI: ReadonlySet = plugins +import { ExternalPluginDefine } from './External/define' import { Flags } from '../utils/flags' import { EthereumPluginDefine } from './Ethereum/define' import { WalletPluginDefine } from './Wallet/define' @@ -23,6 +24,7 @@ import { VCentPluginDefine } from './VCent/define' import { SnapShotPluginDefine } from './Snapshot/define' sideEffect.then(() => { + plugins.add(ExternalPluginDefine) plugins.add(EthereumPluginDefine) plugins.add(WalletPluginDefine) plugins.add(RedPacketPluginDefine) From 5932675bec02ff1f6b9874f3de5b8c7ddfed2c51 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 31 Mar 2021 13:04:37 +0800 Subject: [PATCH 02/28] feat: add external plugin previewer --- .../external-plugin-previewer/package.json | 15 ++++ .../public/index.html | 12 ++++ .../snowpack.config.js | 14 ++++ .../src/Components/MaskCard.tsx | 28 ++++++++ .../src/Components/index.tsx | 5 ++ .../external-plugin-previewer/src/DOMImpl.tsx | 70 +++++++++++++++++++ .../external-plugin-previewer/src/global.d.ts | 26 +++++++ .../external-plugin-previewer/src/index.tsx | 7 ++ .../src/playground.ts | 29 ++++++++ .../external-plugin-previewer/tsconfig.json | 13 ++++ packages/maskbook/package.json | 3 +- .../components/ExternalPluginRenderer.tsx | 4 +- packages/maskbook/tsconfig.json | 3 +- pnpm-lock.yaml | 40 +++++++++-- pnpm-workspace.yaml | 1 + tsconfig.json | 2 + 16 files changed, 262 insertions(+), 10 deletions(-) create mode 100644 packages/external-plugin-previewer/package.json create mode 100644 packages/external-plugin-previewer/public/index.html create mode 100644 packages/external-plugin-previewer/snowpack.config.js create mode 100644 packages/external-plugin-previewer/src/Components/MaskCard.tsx create mode 100644 packages/external-plugin-previewer/src/Components/index.tsx create mode 100644 packages/external-plugin-previewer/src/DOMImpl.tsx create mode 100644 packages/external-plugin-previewer/src/global.d.ts create mode 100644 packages/external-plugin-previewer/src/index.tsx create mode 100644 packages/external-plugin-previewer/src/playground.ts create mode 100644 packages/external-plugin-previewer/tsconfig.json diff --git a/packages/external-plugin-previewer/package.json b/packages/external-plugin-previewer/package.json new file mode 100644 index 000000000000..0461d5e925ce --- /dev/null +++ b/packages/external-plugin-previewer/package.json @@ -0,0 +1,15 @@ +{ + "name": "@dimensiondev/external-plugin-previewer", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "dev -- snowpack dev" + }, + "dependencies": { + "@dimensiondev/maskbook-shared": "workspace:*", + "ef.js": "^0.13.6" + }, + "devDependencies": { + "snowpack": "^3.0.11" + } +} diff --git a/packages/external-plugin-previewer/public/index.html b/packages/external-plugin-previewer/public/index.html new file mode 100644 index 000000000000..1a99104ee45c --- /dev/null +++ b/packages/external-plugin-previewer/public/index.html @@ -0,0 +1,12 @@ + + + + + + External plugin debug playground + + +
+ + + diff --git a/packages/external-plugin-previewer/snowpack.config.js b/packages/external-plugin-previewer/snowpack.config.js new file mode 100644 index 000000000000..0ee35b8b7403 --- /dev/null +++ b/packages/external-plugin-previewer/snowpack.config.js @@ -0,0 +1,14 @@ +// Snowpack Configuration File +// See all supported options: https://www.snowpack.dev/reference/configuration + +/** @type {import("snowpack").SnowpackUserConfig } */ +module.exports = { + mount: { + public: { url: '/' }, + src: { url: '/dist' }, + }, + plugins: [], + packageOptions: {}, + devOptions: { port: 28194 }, + buildOptions: {}, +} diff --git a/packages/external-plugin-previewer/src/Components/MaskCard.tsx b/packages/external-plugin-previewer/src/Components/MaskCard.tsx new file mode 100644 index 000000000000..0a55094edf81 --- /dev/null +++ b/packages/external-plugin-previewer/src/Components/MaskCard.tsx @@ -0,0 +1,28 @@ +import { Card, CardContent, Typography, CardActions, Button } from '@material-ui/core' +import type { Component } from './index' +export const MaskCard: Component = (props) => { + return ( + + + + {String(props.caption)} + + + {String(props.title)} + + + + + + + + + + ) +} +MaskCard.displayName = 'mask-card' +export interface MaskCardProps { + caption: string + title: string + button: string +} diff --git a/packages/external-plugin-previewer/src/Components/index.tsx b/packages/external-plugin-previewer/src/Components/index.tsx new file mode 100644 index 000000000000..c7d82b836baf --- /dev/null +++ b/packages/external-plugin-previewer/src/Components/index.tsx @@ -0,0 +1,5 @@ +export { MaskCard } from './MaskCard' +export interface Component

{ + (props: P, dispatchEvent: (event: Event) => void): React.ReactChild + displayName: string +} diff --git a/packages/external-plugin-previewer/src/DOMImpl.tsx b/packages/external-plugin-previewer/src/DOMImpl.tsx new file mode 100644 index 000000000000..1343b65dcc63 --- /dev/null +++ b/packages/external-plugin-previewer/src/DOMImpl.tsx @@ -0,0 +1,70 @@ +import { setDOMImpl } from 'ef.js' +import type {} from 'react/experimental' +import type {} from 'react-dom/experimental' +import { createReactRootShadowedPartial, ReactRootShadowed } from '@dimensiondev/maskbook-shared' +import * as Components from './Components' + +const createReactRootShadowed = createReactRootShadowedPartial({ + preventEventPropagationList: [], +}) +export function enable() { + setDOMImpl({ + Node, + document: new Proxy(document, { + get(doc, key) { + if (key === 'createElement') return createElement + const val = (doc as any)[key] + if (typeof val === 'function') return val.bind(doc) + return val + }, + }), + }) +} + +function createElement(element: string, options: ElementCreationOptions) { + element = options.is || element + const _ = shouldRender(element) + const isValid = _ !== unknown + const [nativeTag, Component] = _ + const DOM = document.createElement(nativeTag) + DOM.setAttribute('data-kind', element) + + const shadow = DOM.attachShadow({ mode: 'open' }) + + const props: any = { __proto__: null } + isValid && render(Component, props, shadow) + + // No attributes allowed + DOM.setAttribute = () => {} + + // No need to hook event listeners + + // Hook property access + const proto = Object.getPrototypeOf(DOM) + Object.setPrototypeOf( + DOM, + new Proxy(proto, { + set(target, prop, value, receiver) { + // Forward them instead. + props[prop] = value + isValid && render(Component, props, shadow) + return true + }, + }), + ) + return DOM +} + +function render(f: Components.Component, props: any, shadow: ShadowRoot) { + const root: ReactRootShadowed = (shadow as any).__root || ((shadow as any).__root = createReactRootShadowed(shadow)) + root.render(f(props, (event) => void shadow.host.dispatchEvent(event))) +} + +const unknown = ['div', ((() => '') as any) as Components.Component] as const + +function shouldRender(element: string): readonly [string, Components.Component] { + for (const F of Object.values(Components)) { + if (F.displayName === element) return ['main', F] + } + return unknown +} diff --git a/packages/external-plugin-previewer/src/global.d.ts b/packages/external-plugin-previewer/src/global.d.ts new file mode 100644 index 000000000000..e5ee47d27a87 --- /dev/null +++ b/packages/external-plugin-previewer/src/global.d.ts @@ -0,0 +1,26 @@ +declare module 'ef.js' { + export interface DOMImpl { + Node: typeof Node + document: typeof document + } + export function setDOMImpl(impl: DOMImpl): void + export function create(template: string | TemplateStringsArray): typeof Component + + // Not exported + class Component> { + constructor(options?: ComponentConstructorOptions) + $mount(opt: MountOptions): void + $destroy(): void + $methods: Record + $data: T + $subscribe(key: keyof T, callback: Function): void + $unsubscribe(key: keyof T, callback: Function): void + } + export interface ComponentConstructorOptions { + $data: T + } + export interface MountOptions { + target: Node + } + export function t(template: TemplateStringsArray): typeof Component +} diff --git a/packages/external-plugin-previewer/src/index.tsx b/packages/external-plugin-previewer/src/index.tsx new file mode 100644 index 000000000000..815323e9a232 --- /dev/null +++ b/packages/external-plugin-previewer/src/index.tsx @@ -0,0 +1,7 @@ +/// +export function Renderer(data: RenderData) {} +export interface RenderData { + template: string + script: string + payload: unknown +} diff --git a/packages/external-plugin-previewer/src/playground.ts b/packages/external-plugin-previewer/src/playground.ts new file mode 100644 index 000000000000..753336ad0e64 --- /dev/null +++ b/packages/external-plugin-previewer/src/playground.ts @@ -0,0 +1,29 @@ +import React from 'react' +import { enable } from './DOMImpl' +import { t } from 'ef.js' +import { setupPortalShadowRoot } from '@dimensiondev/maskbook-shared' +setupPortalShadowRoot({ mode: 'open' }, []) + +Object.assign(globalThis, { React }) +enable() + +const HelloWorld = t` +>mask-card + %caption = Caption! + %title = This is preview of id {{payload.id}} + %button = Details + >mask-card + %caption = Caption! + %title = This is preview of id {{payload.id}} + %button = Details +` + +const ins = new HelloWorld() +console.log('ins = ', ((globalThis as any).ins = ins)) +ins.$mount({ target: document.body }) +// will be set by Mask +ins.$data.payload = { + id: 1, +} + +export {} diff --git a/packages/external-plugin-previewer/tsconfig.json b/packages/external-plugin-previewer/tsconfig.json new file mode 100644 index 000000000000..5a5c54905e98 --- /dev/null +++ b/packages/external-plugin-previewer/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src/", + "outDir": "./dist/", + "stripInternal": true + }, + "include": ["./src/**/*"], + "ts-node": { + "transpileOnly": true, + "compilerOptions": { "module": "CommonJS" } + } +} diff --git a/packages/maskbook/package.json b/packages/maskbook/package.json index 43bca59e6d8d..188e88c6cd42 100644 --- a/packages/maskbook/package.json +++ b/packages/maskbook/package.json @@ -10,8 +10,9 @@ "@dimensiondev/common-protocols": "1.6.0-20201027083702-d0ae6e2", "@dimensiondev/contracts": "workspace:*", "@dimensiondev/dashboard": "workspace:*", - "@dimensiondev/holoflows-kit": "0.8.0-20210317064617-6c4792c", "@dimensiondev/icons": "workspace:*", + "@dimensiondev/external-plugin-previewer": "workspace:*", + "@dimensiondev/holoflows-kit": "0.8.0-20210302070358-e90c55c", "@dimensiondev/kit": "0.0.0-20210221102734-0b4a937", "@dimensiondev/mask-plugin-infra": "workspace:*", "@dimensiondev/maskbook-shared": "workspace:*", diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index 4c5dc0c96dd1..0ee6e8697532 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -1,7 +1,8 @@ import type { ExternalPluginLoadDetails, Manifest } from '../types' -import { Card, CardHeader, CardContent, Typography, CardActions, Link } from '@material-ui/core' +import { Card, CardHeader, CardContent, Typography, Link } from '@material-ui/core' import { useAsync } from 'react-use' import Services from '../../../extension/service' + export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { const manifest = useExternalPluginManifest(props.url) if (!manifest) return null @@ -20,7 +21,6 @@ export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { } /> Plugin render area - ) } diff --git a/packages/maskbook/tsconfig.json b/packages/maskbook/tsconfig.json index 7efe6ce0c300..d805f8f6794d 100644 --- a/packages/maskbook/tsconfig.json +++ b/packages/maskbook/tsconfig.json @@ -12,7 +12,8 @@ { "path": "../shared/" }, { "path": "../theme/" }, { "path": "../icons/" }, - { "path": "../plugin-infra" } + { "path": "../plugin-infra" }, + { "path": "../external-plugin-previewer" } ], "ts-node": { "transpileOnly": true, "compilerOptions": { "module": "CommonJS" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee0672a140f4..8b49886aac9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,17 @@ importers: babel-loader: 8.2.2_@babel+core@7.13.16 snowpack: 3.3.5 + packages/external-plugin-previewer: + specifiers: + '@dimensiondev/maskbook-shared': workspace:* + ef.js: ^0.13.6 + snowpack: ^3.0.11 + dependencies: + '@dimensiondev/maskbook-shared': link:../shared + ef.js: 0.13.7 + devDependencies: + snowpack: 3.3.5 + packages/icons: specifiers: snowpack: ^3.3.5 @@ -193,7 +204,8 @@ importers: '@dimensiondev/common-protocols': 1.6.0-20201027083702-d0ae6e2 '@dimensiondev/contracts': workspace:* '@dimensiondev/dashboard': workspace:* - '@dimensiondev/holoflows-kit': 0.8.0-20210317064617-6c4792c + '@dimensiondev/external-plugin-previewer': workspace:* + '@dimensiondev/holoflows-kit': 0.8.0-20210302070358-e90c55c '@dimensiondev/icons': workspace:* '@dimensiondev/kit': 0.0.0-20210221102734-0b4a937 '@dimensiondev/mask-plugin-infra': workspace:* @@ -356,7 +368,8 @@ importers: '@dimensiondev/common-protocols': 1.6.0-20201027083702-d0ae6e2 '@dimensiondev/contracts': link:../contracts '@dimensiondev/dashboard': link:../dashboard - '@dimensiondev/holoflows-kit': 0.8.0-20210317064617-6c4792c_webextension-polyfill@0.8.0 + '@dimensiondev/external-plugin-previewer': link:../external-plugin-previewer + '@dimensiondev/holoflows-kit': 0.8.0-20210302070358-e90c55c_webextension-polyfill@0.8.0 '@dimensiondev/icons': link:../icons '@dimensiondev/kit': 0.0.0-20210221102734-0b4a937 '@dimensiondev/mask-plugin-infra': link:../plugin-infra @@ -2552,8 +2565,8 @@ packages: '@msgpack/msgpack': 1.12.2 dev: false - /@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c: - resolution: {integrity: sha512-zB121+yJ3pVCkByf3LEHBUHEMy5rc/r3e4T5E+92SNb+GH/VowvGubSFPygeM2p5lFMyrO+AlvJRPvWQ8ONJTA==, tarball: download/@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c/0ad5efd95cb3cbb58a6e30983f6e0ad2a2216a1cf67d7903c9e0d935a7b1ae2a} + /@dimensiondev/holoflows-kit/0.8.0-20210302070358-e90c55c_webextension-polyfill@0.8.0: + resolution: {integrity: sha512-mu1/mgzFfZ2lJhW7m3oh4InOFCGUulgX8KXBrZ97B1KduBlJKP3CyUcxkIEYEKX9m/TwAbYKQ/co2SWj37CziA==, tarball: download/@dimensiondev/holoflows-kit/0.8.0-20210302070358-e90c55c/53508fd749cc314d345050fa38cc0c233d6b1ce5a5b8aba2cf52a5a56a785c81} peerDependencies: webextension-polyfill: '*' dependencies: @@ -2564,9 +2577,10 @@ packages: lodash-es: 4.17.21 memorize-decorator: 0.2.4 tslib: 2.2.0 + webextension-polyfill: 0.8.0 dev: false - /@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c_webextension-polyfill@0.8.0: + /@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c: resolution: {integrity: sha512-zB121+yJ3pVCkByf3LEHBUHEMy5rc/r3e4T5E+92SNb+GH/VowvGubSFPygeM2p5lFMyrO+AlvJRPvWQ8ONJTA==, tarball: download/@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c/0ad5efd95cb3cbb58a6e30983f6e0ad2a2216a1cf67d7903c9e0d935a7b1ae2a} peerDependencies: webextension-polyfill: '*' @@ -2578,7 +2592,6 @@ packages: lodash-es: 4.17.21 memorize-decorator: 0.2.4 tslib: 2.2.0 - webextension-polyfill: 0.8.0 dev: false /@dimensiondev/kit/0.0.0-20210221102734-0b4a937: @@ -10897,6 +10910,21 @@ packages: /ee-first/1.1.1: resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} + /ef-core/0.13.7: + resolution: {integrity: sha512-peBjbdcCapnWvMV/PCz+T58UzfRhCfc+fqqeoOsHs/X0oTgR18QDsmf7EFGwerL8n29Kuzb9OHLNsZVFaA7UWw==} + dev: false + + /ef.js/0.13.7: + resolution: {integrity: sha512-oxZU47Rx1Xz+YSwSorAwLHsGbspXgV47imO84OtZo4IJLFRb28ULpYjgtl/IIL0XA5fhsUk72vLGcyQNkVzRCA==} + dependencies: + ef-core: 0.13.7 + eft-parser: 0.13.5 + dev: false + + /eft-parser/0.13.5: + resolution: {integrity: sha512-Hj+GY0mf2NNBcgg0v6DsV0/EYI5xDLUICZ5BSLVC+8uFEzoWpWw2VR6e3eWKgGjL2YEybUdg89UXDEHaBmi03Q==} + dev: false + /electron-to-chromium/1.3.703: resolution: {integrity: sha512-SVBVhNB+4zPL+rvtWLw7PZQkw/Eqj1HQZs22xtcqW36+xoifzEOEEDEpkxSMfB6RFeSIOcG00w6z5mSqLr1Y6w==} dev: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 90bd736d6271..45a423c74106 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,3 +11,4 @@ packages: - 'packages/contracts' - 'packages/plugin-infra' - 'packages/plugins/example' + - 'packages/external-plugin-previewer' diff --git a/tsconfig.json b/tsconfig.json index 538029d84e50..3c109379fcaf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,8 @@ { "path": "./packages/icons/" }, { "path": "./packages/plugin-infra" }, { "path": "./packages/plugins/" } + { "path": "./packages/icons/" }, + { "path": "./packages/external-plugin-previewer" } ], "compilerOptions": { /* Basic Options */ From e3f5785e5c446602698e143556d4c2997d5ba23e Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 31 Mar 2021 17:29:16 +0800 Subject: [PATCH 03/28] feat: able to render plugin into the plugin canvas --- .../external-plugin-previewer/package.json | 4 +- .../external-plugin-previewer/src/DOMImpl.tsx | 24 ++++---- .../external-plugin-previewer/src/index.tsx | 23 +++++++- .../src/playground.ts | 2 - .../components/ExternalPluginRenderer.tsx | 57 ++++++++++++++++--- 5 files changed, 84 insertions(+), 26 deletions(-) diff --git a/packages/external-plugin-previewer/package.json b/packages/external-plugin-previewer/package.json index 0461d5e925ce..13dbd59afddc 100644 --- a/packages/external-plugin-previewer/package.json +++ b/packages/external-plugin-previewer/package.json @@ -11,5 +11,7 @@ }, "devDependencies": { "snowpack": "^3.0.11" - } + }, + "main": "./dist/index.js", + "types": "./dist" } diff --git a/packages/external-plugin-previewer/src/DOMImpl.tsx b/packages/external-plugin-previewer/src/DOMImpl.tsx index 1343b65dcc63..772d6d10cd77 100644 --- a/packages/external-plugin-previewer/src/DOMImpl.tsx +++ b/packages/external-plugin-previewer/src/DOMImpl.tsx @@ -7,19 +7,17 @@ import * as Components from './Components' const createReactRootShadowed = createReactRootShadowedPartial({ preventEventPropagationList: [], }) -export function enable() { - setDOMImpl({ - Node, - document: new Proxy(document, { - get(doc, key) { - if (key === 'createElement') return createElement - const val = (doc as any)[key] - if (typeof val === 'function') return val.bind(doc) - return val - }, - }), - }) -} +setDOMImpl({ + Node, + document: new Proxy(document, { + get(doc, key) { + if (key === 'createElement') return createElement + const val = (doc as any)[key] + if (typeof val === 'function') return val.bind(doc) + return val + }, + }), +}) function createElement(element: string, options: ElementCreationOptions) { element = options.is || element diff --git a/packages/external-plugin-previewer/src/index.tsx b/packages/external-plugin-previewer/src/index.tsx index 815323e9a232..d484fbf502b2 100644 --- a/packages/external-plugin-previewer/src/index.tsx +++ b/packages/external-plugin-previewer/src/index.tsx @@ -1,7 +1,28 @@ /// -export function Renderer(data: RenderData) {} +import { useEffect, useState } from 'react' +import { create } from 'ef.js' +import './DOMImpl' +export function MaskExternalPluginPreviewRenderer({ payload, script, template, onError }: RenderData) { + const [dom, setDOM] = useState(null) + useEffect(() => { + if (!dom) return + // This is safe. ef template does not allow any form of dynamic code execute in the template. + try { + const RemoteContent = create(template) + const instance = new RemoteContent({ $data: { payload } }) + instance.$mount({ target: dom }) + return () => instance.$destroy() + } catch (e) { + onError?.(e) + } + return + }, [dom, onError, payload, template]) + return

setDOM(ref)} /> +} export interface RenderData { template: string + /** Currently not supported */ script: string payload: unknown + onError?(e: Error): void } diff --git a/packages/external-plugin-previewer/src/playground.ts b/packages/external-plugin-previewer/src/playground.ts index 753336ad0e64..e244d78d4522 100644 --- a/packages/external-plugin-previewer/src/playground.ts +++ b/packages/external-plugin-previewer/src/playground.ts @@ -1,11 +1,9 @@ import React from 'react' -import { enable } from './DOMImpl' import { t } from 'ef.js' import { setupPortalShadowRoot } from '@dimensiondev/maskbook-shared' setupPortalShadowRoot({ mode: 'open' }, []) Object.assign(globalThis, { React }) -enable() const HelloWorld = t` >mask-card diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index 0ee6e8697532..5595c89f64a0 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -1,34 +1,73 @@ import type { ExternalPluginLoadDetails, Manifest } from '../types' -import { Card, CardHeader, CardContent, Typography, Link } from '@material-ui/core' -import { useAsync } from 'react-use' +import { Card, CardHeader, CardContent, Typography, Link, Button } from '@material-ui/core' +import { useAsyncRetry } from 'react-use' import Services from '../../../extension/service' +import { MaskExternalPluginPreviewRenderer } from '@dimensiondev/external-plugin-previewer' export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { const manifest = useExternalPluginManifest(props.url) - if (!manifest) return null + const template = useExternalPluginTemplate(props.url, manifest.value, props.metaKey) + const retry = ( + + ) @@ -25,4 +27,5 @@ export interface MaskCardProps { caption: string title: string button: string + href: string } diff --git a/packages/maskbook/package.json b/packages/maskbook/package.json index 188e88c6cd42..948f3e44670f 100644 --- a/packages/maskbook/package.json +++ b/packages/maskbook/package.json @@ -12,7 +12,7 @@ "@dimensiondev/dashboard": "workspace:*", "@dimensiondev/icons": "workspace:*", "@dimensiondev/external-plugin-previewer": "workspace:*", - "@dimensiondev/holoflows-kit": "0.8.0-20210302070358-e90c55c", + "@dimensiondev/holoflows-kit": "0.8.0-20210317064617-6c4792c", "@dimensiondev/kit": "0.0.0-20210221102734-0b4a937", "@dimensiondev/mask-plugin-infra": "workspace:*", "@dimensiondev/maskbook-shared": "workspace:*", diff --git a/packages/maskbook/src/plugin-infra/register.ts b/packages/maskbook/src/plugin-infra/register.ts index b2f33b6b374e..3c4e47d2325e 100644 --- a/packages/maskbook/src/plugin-infra/register.ts +++ b/packages/maskbook/src/plugin-infra/register.ts @@ -1 +1,2 @@ import '@dimensiondev/plugin-example' +import '../plugins/External' diff --git a/packages/maskbook/src/plugins/External/Dashboard/index.tsx b/packages/maskbook/src/plugins/External/Dashboard/index.tsx new file mode 100644 index 000000000000..12ba2a34c876 --- /dev/null +++ b/packages/maskbook/src/plugins/External/Dashboard/index.tsx @@ -0,0 +1,9 @@ +import type { Plugin } from '@dimensiondev/mask-plugin-infra' +import { base } from '../base' + +const dashboard: Plugin.Dashboard.Definition = { + ...base, + init(signal) {}, +} + +export default dashboard diff --git a/packages/maskbook/src/plugins/External/define.tsx b/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx similarity index 68% rename from packages/maskbook/src/plugins/External/define.tsx rename to packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx index f31227168cc0..913fd3b59e2f 100644 --- a/packages/maskbook/src/plugins/External/define.tsx +++ b/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx @@ -1,13 +1,13 @@ +import type { Plugin } from '@dimensiondev/mask-plugin-infra' import { Suspense } from 'react' -import { PluginConfig, PluginScope, PluginStage } from '../types' -import { ExternalPluginLoader } from './Container' -import type { ExternalPluginLoadDetails } from './types' -export const ExternalPluginDefine: PluginConfig = { - identifier: 'com.maskbook.external', - pluginName: 'External Plugin', - scope: PluginScope.Public, - stage: PluginStage.Production, - successDecryptionInspector: function Comp(props) { +import { ExternalPluginLoader } from '../components/Container' +import type { ExternalPluginLoadDetails } from '../types' +import { base } from '../base' + +const sns: Plugin.SNSAdaptor.Definition = { + ...base, + init(signal) {}, + DecryptedInspector: function Comp(props) { const tm = props.message if (!tm.meta) return null let JSX: ExternalPluginLoadDetails[] = [] @@ -24,6 +24,8 @@ export const ExternalPluginDefine: PluginConfig = { ) }, } + +export default sns // plugin:dimensiondev.github.io/Mask-Plugin-Example/@v1 function parse(x: string) { let [address, ...key] = x.slice('plugin:'.length).split('@') diff --git a/packages/maskbook/src/plugins/External/Worker/index.ts b/packages/maskbook/src/plugins/External/Worker/index.ts new file mode 100644 index 000000000000..c982cf1e4865 --- /dev/null +++ b/packages/maskbook/src/plugins/External/Worker/index.ts @@ -0,0 +1,8 @@ +import type { Plugin } from '@dimensiondev/mask-plugin-infra' +import { base } from '../base' + +const worker: Plugin.Worker.Definition = { + ...base, + init(signal) {}, +} +export default worker diff --git a/packages/maskbook/src/plugins/External/base.ts b/packages/maskbook/src/plugins/External/base.ts new file mode 100644 index 000000000000..b7a1db986701 --- /dev/null +++ b/packages/maskbook/src/plugins/External/base.ts @@ -0,0 +1,14 @@ +import type { Plugin } from '@dimensiondev/mask-plugin-infra' + +export const base: Plugin.Shared.Definition = { + ID: 'io.maskbook.external', + icon: '🧩', + name: { fallback: 'Mask External Plugin Loader' }, + description: { fallback: 'Able to load external plugins.' }, + publisher: { name: { fallback: 'Mask Network' }, link: 'https://mask.io/' }, + enableRequirement: { + architecture: { app: true, web: true }, + networks: { type: 'opt-out', networks: {} }, + target: 'insider', + }, +} diff --git a/packages/maskbook/src/plugins/External/Container.tsx b/packages/maskbook/src/plugins/External/components/Container.tsx similarity index 87% rename from packages/maskbook/src/plugins/External/Container.tsx rename to packages/maskbook/src/plugins/External/components/Container.tsx index b8471c27990a..31e474e47b72 100644 --- a/packages/maskbook/src/plugins/External/Container.tsx +++ b/packages/maskbook/src/plugins/External/components/Container.tsx @@ -1,9 +1,9 @@ import { SnackbarContent } from '@material-ui/core' import { useAsyncRetry } from 'react-use' import { Suspense, useRef } from 'react' -import type { ExternalPluginLoadDetails } from './types' -import { UnknownPluginLoadRequestUI } from './components/UnknownPluginLoadRequest' -import { ExternalPluginRenderer } from './components/ExternalPluginRenderer' +import type { ExternalPluginLoadDetails } from '../types' +import { UnknownPluginLoadRequestUI } from './UnknownPluginLoadRequest' +import { ExternalPluginRenderer } from './ExternalPluginRenderer' export interface ExternalPluginContainerProps { plugins: ExternalPluginLoadDetails[] } diff --git a/packages/maskbook/src/plugins/External/index.ts b/packages/maskbook/src/plugins/External/index.ts new file mode 100644 index 000000000000..6448de6b8ea4 --- /dev/null +++ b/packages/maskbook/src/plugins/External/index.ts @@ -0,0 +1,11 @@ +import { registerPlugin } from '@dimensiondev/mask-plugin-infra' +import { base } from './base' + +registerPlugin({ + ...base, + SNSAdaptor: { + load: () => import('./SNSAdaptor'), + hotModuleReload: (hot) => + module.hot && module.hot.accept('./SNSAdaptor/index', () => hot(import('./SNSAdaptor'))), + }, +}) diff --git a/packages/maskbook/src/plugins/PluginUI.ts b/packages/maskbook/src/plugins/PluginUI.ts index f16d037432ad..998a13fb5493 100644 --- a/packages/maskbook/src/plugins/PluginUI.ts +++ b/packages/maskbook/src/plugins/PluginUI.ts @@ -4,7 +4,6 @@ import type { PluginConfig } from './types' const plugins = new Set() export const PluginUI: ReadonlySet = plugins -import { ExternalPluginDefine } from './External/define' import { Flags } from '../utils/flags' import { EthereumPluginDefine } from './Ethereum/define' import { WalletPluginDefine } from './Wallet/define' @@ -24,7 +23,6 @@ import { VCentPluginDefine } from './VCent/define' import { SnapShotPluginDefine } from './Snapshot/define' sideEffect.then(() => { - plugins.add(ExternalPluginDefine) plugins.add(EthereumPluginDefine) plugins.add(WalletPluginDefine) plugins.add(RedPacketPluginDefine) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b49886aac9f..1a7639d7406e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,7 +205,7 @@ importers: '@dimensiondev/contracts': workspace:* '@dimensiondev/dashboard': workspace:* '@dimensiondev/external-plugin-previewer': workspace:* - '@dimensiondev/holoflows-kit': 0.8.0-20210302070358-e90c55c + '@dimensiondev/holoflows-kit': 0.8.0-20210317064617-6c4792c '@dimensiondev/icons': workspace:* '@dimensiondev/kit': 0.0.0-20210221102734-0b4a937 '@dimensiondev/mask-plugin-infra': workspace:* @@ -369,7 +369,7 @@ importers: '@dimensiondev/contracts': link:../contracts '@dimensiondev/dashboard': link:../dashboard '@dimensiondev/external-plugin-previewer': link:../external-plugin-previewer - '@dimensiondev/holoflows-kit': 0.8.0-20210302070358-e90c55c_webextension-polyfill@0.8.0 + '@dimensiondev/holoflows-kit': 0.8.0-20210317064617-6c4792c_webextension-polyfill@0.8.0 '@dimensiondev/icons': link:../icons '@dimensiondev/kit': 0.0.0-20210221102734-0b4a937 '@dimensiondev/mask-plugin-infra': link:../plugin-infra @@ -2565,8 +2565,8 @@ packages: '@msgpack/msgpack': 1.12.2 dev: false - /@dimensiondev/holoflows-kit/0.8.0-20210302070358-e90c55c_webextension-polyfill@0.8.0: - resolution: {integrity: sha512-mu1/mgzFfZ2lJhW7m3oh4InOFCGUulgX8KXBrZ97B1KduBlJKP3CyUcxkIEYEKX9m/TwAbYKQ/co2SWj37CziA==, tarball: download/@dimensiondev/holoflows-kit/0.8.0-20210302070358-e90c55c/53508fd749cc314d345050fa38cc0c233d6b1ce5a5b8aba2cf52a5a56a785c81} + /@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c: + resolution: {integrity: sha512-zB121+yJ3pVCkByf3LEHBUHEMy5rc/r3e4T5E+92SNb+GH/VowvGubSFPygeM2p5lFMyrO+AlvJRPvWQ8ONJTA==, tarball: download/@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c/0ad5efd95cb3cbb58a6e30983f6e0ad2a2216a1cf67d7903c9e0d935a7b1ae2a} peerDependencies: webextension-polyfill: '*' dependencies: @@ -2577,10 +2577,9 @@ packages: lodash-es: 4.17.21 memorize-decorator: 0.2.4 tslib: 2.2.0 - webextension-polyfill: 0.8.0 dev: false - /@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c: + /@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c_webextension-polyfill@0.8.0: resolution: {integrity: sha512-zB121+yJ3pVCkByf3LEHBUHEMy5rc/r3e4T5E+92SNb+GH/VowvGubSFPygeM2p5lFMyrO+AlvJRPvWQ8ONJTA==, tarball: download/@dimensiondev/holoflows-kit/0.8.0-20210317064617-6c4792c/0ad5efd95cb3cbb58a6e30983f6e0ad2a2216a1cf67d7903c9e0d935a7b1ae2a} peerDependencies: webextension-polyfill: '*' @@ -2592,6 +2591,7 @@ packages: lodash-es: 4.17.21 memorize-decorator: 0.2.4 tslib: 2.2.0 + webextension-polyfill: 0.8.0 dev: false /@dimensiondev/kit/0.0.0-20210221102734-0b4a937: From b319dbaae1f8eaa5a8ea38dcdfc95bec2d2e7534 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 7 May 2021 13:20:17 +0800 Subject: [PATCH 06/28] feat: internally convert dom nodes to ef template --- .../components/ExternalPluginRenderer.tsx | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index 5595c89f64a0..da825d829920 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -63,11 +63,42 @@ async function fetchTemplate(url: string | null) { if (!url) return const blob = await Services.Helper.fetch(url) const text = await blob.text() - const dom = new DOMParser().parseFromString(text, 'text/html') - const template = dom.querySelector('template') + const parser = new DOMParser() + const dom = parser.parseFromString(text, 'text/html').querySelector('template')?.innerHTML + if (!dom) return null + const template = parser.parseFromString(dom, 'text/html').querySelector('body')?.childNodes if (!template) return null - // @ts-ignore Property 'replaceAll' does not exist on type 'string'. Do you need to change your target library? Try changing the `lib` compiler option to 'es2021' or later.ts(2550) - return template.innerHTML.replaceAll('>', '>') + console.log(htmlToTemplate(template)) + return htmlToTemplate(template) +} + +const indentLevel = 2 +function htmlToTemplate(top: NodeListOf) { + function* text(indent: number, text: Text) { + for (const line of text.textContent?.split('\n') || '') { + yield `${getIndent(indent)}. ${line}` + } + } + function* html(indent: number, node: HTMLElement): Generator { + yield `${getIndent(indent)}>${node.tagName.toLowerCase()}` + for (const attr of node.attributes) { + yield `${getIndent(indent + indentLevel)}%${attr.name} = ${attr.value}` + } + yield* convertList(indent + indentLevel, node.childNodes) + } + function* convertList(indent: number, nodes: NodeListOf): Generator { + for (const node of nodes) { + if (isText(node)) yield* text(indent, node) + else if (node instanceof HTMLElement) yield* html(indent, node) + } + } + function getIndent(x: number) { + return ' '.repeat(x) + } + function isText(node: ChildNode): node is Text { + return node.nodeType === document.TEXT_NODE + } + return [...convertList(0, top)].join('\n') } async function fetchManifest(addr: string) { From 882e675aaa7e81c42db55aff7f0582a65b8f6ce9 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 7 May 2021 14:58:33 +0800 Subject: [PATCH 07/28] feat: add a permission guard --- .../popups/MissingParameter/index.tsx | 12 +++++++ .../popups/PermissionAwareRedirect/index.tsx | 34 +++++++++++++++++++ .../popups/PermissionAwareRedirect/ui.tsx | 29 ++++++++++++++++ .../popups/PermissionAwareRedirect/utils.ts | 12 +++++++ .../maskbook/src/extension/popups/index.tsx | 8 ++++- .../maskbook/src/extension/popups/render.tsx | 10 ++++-- packages/maskbook/webpack.config.ts | 3 ++ 7 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 packages/maskbook/src/extension/popups/MissingParameter/index.tsx create mode 100644 packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx create mode 100644 packages/maskbook/src/extension/popups/PermissionAwareRedirect/ui.tsx create mode 100644 packages/maskbook/src/extension/popups/PermissionAwareRedirect/utils.ts diff --git a/packages/maskbook/src/extension/popups/MissingParameter/index.tsx b/packages/maskbook/src/extension/popups/MissingParameter/index.tsx new file mode 100644 index 000000000000..389e42db4b1c --- /dev/null +++ b/packages/maskbook/src/extension/popups/MissingParameter/index.tsx @@ -0,0 +1,12 @@ +import { Card, CardContent, Typography } from '@material-ui/core' + +export function MissingParameter(props: { message: string }) { + return ( + + + {props.message} + Please close this page. + + + ) +} diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx new file mode 100644 index 000000000000..b31b51d93ecb --- /dev/null +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx @@ -0,0 +1,34 @@ +export { PermissionAwareRedirectUI } from './ui' + +import { useEffect } from 'react' +import { useLocation } from 'react-router' +import { useAsync } from 'react-use' +import { MissingParameter } from '../MissingParameter' +import { PermissionAwareRedirectUI } from './ui' +import { getHostPermissionFieldFromURL, isValidURL } from './utils' +export default function PermissionAwareRedirect() { + const url = new URLSearchParams(useLocation().search).get('url') + if (!url) return + if (!isValidURL(url)) return + return +} +function Inner({ url }: { url: string }) { + const { value: hasPermission } = useAsync(async () => { + if (!url) return false + return browser.permissions.contains({ origins: [getHostPermissionFieldFromURL(url)] }) + }, [url]) + useEffect(() => { + if (hasPermission) { + location.href = url + } + }, [hasPermission, url]) + return ( + { + browser.permissions.request({ origins: [getHostPermissionFieldFromURL(url)] }) + }} + /> + ) +} diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/ui.tsx b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/ui.tsx new file mode 100644 index 000000000000..e19105325d6c --- /dev/null +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/ui.tsx @@ -0,0 +1,29 @@ +import { Typography, Card, CardContent, Button, CardActions } from '@material-ui/core' + +export function PermissionAwareRedirectUI(props: PermissionAwareRedirectProps) { + if (props.granted) { + return ( + <> + Redirect to {props.url}. +
+ If your browser does not redirect, please click here. + + ) + } + return ( + + + Mask need permission for: + {props.url} + + + + + + ) +} +export interface PermissionAwareRedirectProps { + url: string + onRequest(): void + granted: boolean +} diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/utils.ts b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/utils.ts new file mode 100644 index 000000000000..f4e9c40874d5 --- /dev/null +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/utils.ts @@ -0,0 +1,12 @@ +export function getHostPermissionFieldFromURL(url: string) { + const u = new URL(url) + return `*://${u.hostname}/*` +} +export function isValidURL(url: string): boolean { + try { + const u = new URL(url) + return u.protocol.startsWith('http') + } catch { + return false + } +} diff --git a/packages/maskbook/src/extension/popups/index.tsx b/packages/maskbook/src/extension/popups/index.tsx index fdcc470aa19a..038bbfceaa35 100644 --- a/packages/maskbook/src/extension/popups/index.tsx +++ b/packages/maskbook/src/extension/popups/index.tsx @@ -1 +1,7 @@ -export enum DialogRoutes {} +export function PermissionAwareRedirectOf(url: string) { + return `${DialogRoutes.PermissionAwareRedirect}?url=${encodeURIComponent(url)}` +} +/** Do not use this directly */ +export enum DialogRoutes { + PermissionAwareRedirect = '/redirect', +} diff --git a/packages/maskbook/src/extension/popups/render.tsx b/packages/maskbook/src/extension/popups/render.tsx index 571f61465d91..b8ecee642b05 100644 --- a/packages/maskbook/src/extension/popups/render.tsx +++ b/packages/maskbook/src/extension/popups/render.tsx @@ -1,21 +1,25 @@ /// /// -import { Suspense } from 'react' -import { Switch } from 'react-router' +import { lazy, Suspense } from 'react' +import { Route, Switch } from 'react-router' import { HashRouter } from 'react-router-dom' import ReactDOM from 'react-dom' import { MaskUIRoot } from '../../UIRoot' +import { DialogRoutes } from '.' const root = document.createElement('div') document.body.insertBefore(root, document.body.children[0] || null) ReactDOM.unstable_createRoot(root).render() +const PermissionAwareRedirect = lazy(() => import('./PermissionAwareRedirect')) function Dialogs() { return MaskUIRoot( - + + } exact /> + , ) diff --git a/packages/maskbook/webpack.config.ts b/packages/maskbook/webpack.config.ts index c8308317ac17..9688f498e55e 100644 --- a/packages/maskbook/webpack.config.ts +++ b/packages/maskbook/webpack.config.ts @@ -101,6 +101,9 @@ function config(opts: { '@dimensiondev/icons': require.resolve('../icons/index.ts'), '@dimensiondev/mask-plugin-infra': require.resolve('../plugin-infra/src/index.ts'), '@dimensiondev/plugin-example': require.resolve('../plugins/example/src/index.ts'), + '@dimensiondev/external-plugin-previewer': require.resolve( + '../external-plugin-previewer/src/index.tsx', + ), }, // Polyfill those Node built-ins fallback: { From 8e86fb9c855e75ab7073ca9469abb8443cdb6859 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 7 May 2021 15:33:51 +0800 Subject: [PATCH 08/28] feat: support permission grant --- .../src/Components/MaskCard.tsx | 3 ++- packages/external-plugin-previewer/src/host.ts | 12 ++++++++++++ packages/external-plugin-previewer/src/index.tsx | 2 ++ .../src/extension/background-script/HelperService.ts | 9 +++++++++ .../popups/PermissionAwareRedirect/index.tsx | 6 +++--- packages/maskbook/src/extension/popups/index.tsx | 7 +++---- .../External/components/ExternalPluginRenderer.tsx | 9 ++++++++- 7 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 packages/external-plugin-previewer/src/host.ts diff --git a/packages/external-plugin-previewer/src/Components/MaskCard.tsx b/packages/external-plugin-previewer/src/Components/MaskCard.tsx index 130e98d2787d..ab683d0703f0 100644 --- a/packages/external-plugin-previewer/src/Components/MaskCard.tsx +++ b/packages/external-plugin-previewer/src/Components/MaskCard.tsx @@ -1,4 +1,5 @@ import { Card, CardContent, Typography, CardActions, Button } from '@material-ui/core' +import { hostConfig } from '../host' import type { Component } from './index' export const MaskCard: Component = (props) => { return ( @@ -15,7 +16,7 @@ export const MaskCard: Component = (props) => { - diff --git a/packages/external-plugin-previewer/src/host.ts b/packages/external-plugin-previewer/src/host.ts new file mode 100644 index 000000000000..b4ca0a789685 --- /dev/null +++ b/packages/external-plugin-previewer/src/host.ts @@ -0,0 +1,12 @@ +/** @internal */ +export const hostConfig: HostConfig = { + permissionAwareOpen(url: string) { + return url + }, +} +export interface HostConfig { + permissionAwareOpen(url: string): void +} +export function setHostConfig(host: HostConfig) { + hostConfig.permissionAwareOpen = host.permissionAwareOpen +} diff --git a/packages/external-plugin-previewer/src/index.tsx b/packages/external-plugin-previewer/src/index.tsx index d484fbf502b2..18f935c696cd 100644 --- a/packages/external-plugin-previewer/src/index.tsx +++ b/packages/external-plugin-previewer/src/index.tsx @@ -1,3 +1,5 @@ +export { setHostConfig } from './host' +export type { HostConfig } from './host' /// import { useEffect, useState } from 'react' import { create } from 'ef.js' diff --git a/packages/maskbook/src/extension/background-script/HelperService.ts b/packages/maskbook/src/extension/background-script/HelperService.ts index 30349420f3e2..dde4134ecada 100644 --- a/packages/maskbook/src/extension/background-script/HelperService.ts +++ b/packages/maskbook/src/extension/background-script/HelperService.ts @@ -39,3 +39,12 @@ export function saveAsFileFromBuffer(file: BufferSource, mimeType: string, fileN const url = URL.createObjectURL(blob) saveAsFileFromUrl(url, fileName) } + +export function openDialogPopup(url: string) { + browser.windows.create({ + type: 'popup', + width: 400, + height: 600, + url: browser.runtime.getURL('/popups.html#' + url), + }) +} diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx index b31b51d93ecb..79f711431a25 100644 --- a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx @@ -2,7 +2,7 @@ export { PermissionAwareRedirectUI } from './ui' import { useEffect } from 'react' import { useLocation } from 'react-router' -import { useAsync } from 'react-use' +import { useAsyncRetry } from 'react-use' import { MissingParameter } from '../MissingParameter' import { PermissionAwareRedirectUI } from './ui' import { getHostPermissionFieldFromURL, isValidURL } from './utils' @@ -13,7 +13,7 @@ export default function PermissionAwareRedirect() { return } function Inner({ url }: { url: string }) { - const { value: hasPermission } = useAsync(async () => { + const { value: hasPermission, retry } = useAsyncRetry(async () => { if (!url) return false return browser.permissions.contains({ origins: [getHostPermissionFieldFromURL(url)] }) }, [url]) @@ -27,7 +27,7 @@ function Inner({ url }: { url: string }) { url={url} granted={!!hasPermission} onRequest={() => { - browser.permissions.request({ origins: [getHostPermissionFieldFromURL(url)] }) + browser.permissions.request({ origins: [getHostPermissionFieldFromURL(url)] }).finally(retry) }} /> ) diff --git a/packages/maskbook/src/extension/popups/index.tsx b/packages/maskbook/src/extension/popups/index.tsx index 038bbfceaa35..37be4975de07 100644 --- a/packages/maskbook/src/extension/popups/index.tsx +++ b/packages/maskbook/src/extension/popups/index.tsx @@ -1,7 +1,6 @@ -export function PermissionAwareRedirectOf(url: string) { - return `${DialogRoutes.PermissionAwareRedirect}?url=${encodeURIComponent(url)}` -} -/** Do not use this directly */ export enum DialogRoutes { PermissionAwareRedirect = '/redirect', } +export function PermissionAwareRedirectOf(url: string) { + return `${DialogRoutes.PermissionAwareRedirect}?url=${encodeURIComponent(url)}` +} diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index da825d829920..f57fa29178e5 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -2,7 +2,14 @@ import type { ExternalPluginLoadDetails, Manifest } from '../types' import { Card, CardHeader, CardContent, Typography, Link, Button } from '@material-ui/core' import { useAsyncRetry } from 'react-use' import Services from '../../../extension/service' -import { MaskExternalPluginPreviewRenderer } from '@dimensiondev/external-plugin-previewer' +import { MaskExternalPluginPreviewRenderer, setHostConfig } from '@dimensiondev/external-plugin-previewer' +import { PermissionAwareRedirectOf } from '../../../extension/popups' + +setHostConfig({ + permissionAwareOpen(url) { + Services.Helper.openDialogPopup(PermissionAwareRedirectOf(url)) + }, +}) export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { const manifest = useExternalPluginManifest(props.url) From 10851ccce8888b98cbbd6440aae26cd4ed9464eb Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 7 May 2021 23:36:51 +0800 Subject: [PATCH 09/28] feat: add sdk entry --- packages/maskbook/src/extension/external-sdk/README.md | 1 + packages/maskbook/src/extension/external-sdk/index.ts | 1 + packages/maskbook/webpack.config.ts | 5 +++++ 3 files changed, 7 insertions(+) create mode 100644 packages/maskbook/src/extension/external-sdk/README.md create mode 100644 packages/maskbook/src/extension/external-sdk/index.ts diff --git a/packages/maskbook/src/extension/external-sdk/README.md b/packages/maskbook/src/extension/external-sdk/README.md new file mode 100644 index 000000000000..695b28bad96d --- /dev/null +++ b/packages/maskbook/src/extension/external-sdk/README.md @@ -0,0 +1 @@ +This entry is used to load Mask SDK into the webpage. It will be loaded on demand. diff --git a/packages/maskbook/src/extension/external-sdk/index.ts b/packages/maskbook/src/extension/external-sdk/index.ts new file mode 100644 index 000000000000..950976e1f730 --- /dev/null +++ b/packages/maskbook/src/extension/external-sdk/index.ts @@ -0,0 +1 @@ +console.log('sdk running!') diff --git a/packages/maskbook/webpack.config.ts b/packages/maskbook/webpack.config.ts index 9688f498e55e..0779bfae43a9 100644 --- a/packages/maskbook/webpack.config.ts +++ b/packages/maskbook/webpack.config.ts @@ -284,6 +284,7 @@ export default async function (cli_env: Record = {}, argv: { mo 'background-service': withBrowserPolyfill(src('./src/background-service.ts')), debug: withBrowserPolyfill(src('./src/extension/debug-page')), popups: withBrowserPolyfill(src('./src/extension/popups/render.tsx')), + 'content-script-external-sdk': withBrowserPolyfill(src('./src/extension/external-sdk')), } if (isManifestV3) delete main.entry['background-script'] if (mode === 'production') delete main.entry['dashboard-next'] @@ -296,6 +297,10 @@ export default async function (cli_env: Record = {}, argv: { mo getHTMLPlugin({ chunks: ['content-script'], filename: 'generated__content__script.html' }), getHTMLPlugin({ chunks: ['debug'], filename: 'debug.html' }), getHTMLPlugin({ chunks: ['popups'], filename: 'popups.html' }), + getHTMLPlugin({ + chunks: ['content-script-external-sdk'], + filename: 'generated__content__script__sdk.html', + }), ) // generate pages for each entry if (mode === 'development') main.plugins!.push(getHTMLPlugin({ chunks: ['dashboard-next'], filename: 'next.html' })) From 7fc01525e560fcec9621fe8d6cf7ca442f26b110 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Sat, 8 May 2021 13:03:09 +0800 Subject: [PATCH 10/28] refactor: change inject content scripts --- .../src/extension/background-script/Jobs/InjectContentScripts.ts | 1 + packages/maskbook/src/extension/external-sdk/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts index e8d307fd9153..5aefc934a430 100644 --- a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts +++ b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts @@ -4,6 +4,7 @@ type Args = browser.webNavigation.TransitionNavListener extends browser.webNavig export default function () { const injectedScript = fetchInjectedScript() const contentScripts = fetchInjectContentScript('/generated__content__script.html') + // const sdk = fetchInjectContentScript('/generated__content__script__sdk.html') async function onCommittedListener(arg: Args): Promise { if (arg.url === 'about:blank') return if (!arg.url.startsWith('http')) return diff --git a/packages/maskbook/src/extension/external-sdk/index.ts b/packages/maskbook/src/extension/external-sdk/index.ts index 950976e1f730..d74d084d2706 100644 --- a/packages/maskbook/src/extension/external-sdk/index.ts +++ b/packages/maskbook/src/extension/external-sdk/index.ts @@ -1 +1,2 @@ console.log('sdk running!') +export {} From 015305c7ccaad49d8a9b84e17ddc24bac43be8de Mon Sep 17 00:00:00 2001 From: Jack Works Date: Sat, 8 May 2021 13:54:40 +0800 Subject: [PATCH 11/28] feat: add a mech for enable sdk --- .../src/extension/background-script/HelperService.ts | 7 +++++++ .../background-script/Jobs/InjectContentScripts.ts | 6 +++++- .../src/extension/popups/PermissionAwareRedirect/index.tsx | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/maskbook/src/extension/background-script/HelperService.ts b/packages/maskbook/src/extension/background-script/HelperService.ts index dde4134ecada..d0ea8a36a68f 100644 --- a/packages/maskbook/src/extension/background-script/HelperService.ts +++ b/packages/maskbook/src/extension/background-script/HelperService.ts @@ -1,4 +1,5 @@ import { memoizePromise } from '../../utils/memoize' +import { getHostPermissionFieldFromURL } from '../popups/PermissionAwareRedirect/utils' const cache = new Map() export const resolveTCOLink = memoizePromise( @@ -48,3 +49,9 @@ export function openDialogPopup(url: string) { url: browser.runtime.getURL('/popups.html#' + url), }) } +export async function enableSDK(url: string) { + sessionStorage.setItem('sdk:' + getHostPermissionFieldFromURL(url), '1') +} +export function isSDKEnabled(url: string) { + return !!sessionStorage.getItem('sdk:' + getHostPermissionFieldFromURL(url)) +} diff --git a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts index 5aefc934a430..2956d40fc38a 100644 --- a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts +++ b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts @@ -1,10 +1,11 @@ import { Flags } from '../../../utils/flags' +import { isSDKEnabled } from '../HelperService' type Args = browser.webNavigation.TransitionNavListener extends browser.webNavigation.NavListener ? U : never export default function () { const injectedScript = fetchInjectedScript() const contentScripts = fetchInjectContentScript('/generated__content__script.html') - // const sdk = fetchInjectContentScript('/generated__content__script__sdk.html') + const sdk = fetchInjectContentScript('/generated__content__script__sdk.html') async function onCommittedListener(arg: Args): Promise { if (arg.url === 'about:blank') return if (!arg.url.startsWith('http')) return @@ -32,6 +33,9 @@ export default function () { }) } contentScripts(arg.tabId, arg.frameId).catch(HandleError(arg)) + if (isSDKEnabled(arg.url)) { + sdk(arg.tabId, arg.frameId).catch(HandleError(arg)) + } } browser.webNavigation.onCommitted.addListener(onCommittedListener) return () => browser.webNavigation.onCommitted.removeListener(onCommittedListener) diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx index 79f711431a25..962a9235d7b6 100644 --- a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx @@ -3,6 +3,7 @@ export { PermissionAwareRedirectUI } from './ui' import { useEffect } from 'react' import { useLocation } from 'react-router' import { useAsyncRetry } from 'react-use' +import Services from '../../service' import { MissingParameter } from '../MissingParameter' import { PermissionAwareRedirectUI } from './ui' import { getHostPermissionFieldFromURL, isValidURL } from './utils' @@ -19,7 +20,7 @@ function Inner({ url }: { url: string }) { }, [url]) useEffect(() => { if (hasPermission) { - location.href = url + Services.Helper.enableSDK(url).then(() => (location.href = url)) } }, [hasPermission, url]) return ( From d9dbd34e10795b40f47d78b796a4181d9638a29e Mon Sep 17 00:00:00 2001 From: Jack Works Date: Sat, 8 May 2021 14:31:50 +0800 Subject: [PATCH 12/28] feat: add basic sdk --- .../src/extension/external-sdk/index.ts | 23 +++++++++++++++++-- .../src/extension/external-sdk/sdk.ts | 4 ++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 packages/maskbook/src/extension/external-sdk/sdk.ts diff --git a/packages/maskbook/src/extension/external-sdk/index.ts b/packages/maskbook/src/extension/external-sdk/index.ts index d74d084d2706..3a773e71b2ca 100644 --- a/packages/maskbook/src/extension/external-sdk/index.ts +++ b/packages/maskbook/src/extension/external-sdk/index.ts @@ -1,2 +1,21 @@ -console.log('sdk running!') -export {} +import { AsyncCall, JSONSerialization, EventBasedChannel } from 'async-call-rpc' +import * as SDK from './sdk' + +console.log('SDK server started') +const channel: EventBasedChannel = { + on(listener) { + const l = (x: Event) => x instanceof CustomEvent && listener(x.detail) + document.addEventListener('mask-in', l) + return () => document.removeEventListener('mask-in', l) + }, + send(message) { + document.dispatchEvent(new CustomEvent('mask-out', { detail: message })) + }, +} + +AsyncCall(SDK, { + serializer: JSONSerialization(undefined), + channel, + log: false, +}) +document.dispatchEvent(new Event('mask-start')) diff --git a/packages/maskbook/src/extension/external-sdk/sdk.ts b/packages/maskbook/src/extension/external-sdk/sdk.ts new file mode 100644 index 000000000000..56396edd1977 --- /dev/null +++ b/packages/maskbook/src/extension/external-sdk/sdk.ts @@ -0,0 +1,4 @@ +/** Version of this SDK */ +export function version() { + return 1 +} From bc0c46237cf0c3eb0b9313dff50c3c7bf0ad9152 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Sat, 8 May 2021 14:56:24 +0800 Subject: [PATCH 13/28] feat: add sdk --- packages/maskbook/src/extension/external-sdk/hmr-sdk.ts | 8 ++++++++ packages/maskbook/src/extension/external-sdk/index.ts | 2 +- packages/maskbook/src/extension/external-sdk/sdk.ts | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 packages/maskbook/src/extension/external-sdk/hmr-sdk.ts diff --git a/packages/maskbook/src/extension/external-sdk/hmr-sdk.ts b/packages/maskbook/src/extension/external-sdk/hmr-sdk.ts new file mode 100644 index 000000000000..2c038b2409de --- /dev/null +++ b/packages/maskbook/src/extension/external-sdk/hmr-sdk.ts @@ -0,0 +1,8 @@ +import * as SDK from './sdk' +const hmrSDK = { ...SDK } +export default hmrSDK +if (module.hot) { + module.hot.accept('./sdk', async () => { + Object.assign(hmrSDK, await import('./sdk')) + }) +} diff --git a/packages/maskbook/src/extension/external-sdk/index.ts b/packages/maskbook/src/extension/external-sdk/index.ts index 3a773e71b2ca..81f1053598cf 100644 --- a/packages/maskbook/src/extension/external-sdk/index.ts +++ b/packages/maskbook/src/extension/external-sdk/index.ts @@ -1,5 +1,5 @@ import { AsyncCall, JSONSerialization, EventBasedChannel } from 'async-call-rpc' -import * as SDK from './sdk' +import SDK from './hmr-sdk' console.log('SDK server started') const channel: EventBasedChannel = { diff --git a/packages/maskbook/src/extension/external-sdk/sdk.ts b/packages/maskbook/src/extension/external-sdk/sdk.ts index 56396edd1977..daee19671967 100644 --- a/packages/maskbook/src/extension/external-sdk/sdk.ts +++ b/packages/maskbook/src/extension/external-sdk/sdk.ts @@ -1,4 +1,7 @@ /** Version of this SDK */ -export function version() { +export async function version() { return 1 } +export async function echo(x: T) { + return x +} From 45c55ea48b745b410d5c11933674e6116e8f426d Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 13 May 2021 15:33:52 +0800 Subject: [PATCH 14/28] fix: prettier --- packages/external-plugin-previewer/src/DOMImpl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/external-plugin-previewer/src/DOMImpl.tsx b/packages/external-plugin-previewer/src/DOMImpl.tsx index 772d6d10cd77..7e4092e6c454 100644 --- a/packages/external-plugin-previewer/src/DOMImpl.tsx +++ b/packages/external-plugin-previewer/src/DOMImpl.tsx @@ -58,7 +58,7 @@ function render(f: Components.Component, props: any, shadow: ShadowRoot) { root.render(f(props, (event) => void shadow.host.dispatchEvent(event))) } -const unknown = ['div', ((() => '') as any) as Components.Component] as const +const unknown = ['div', (() => '') as any as Components.Component] as const function shouldRender(element: string): readonly [string, Components.Component] { for (const F of Object.values(Components)) { From e5fb9d5f5fc3f3d490bb892bbaeabb66d13cef57 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 19 May 2021 13:30:29 +0800 Subject: [PATCH 15/28] chore: change some custom dom --- .../src/Components/MaskCard.tsx | 2 +- .../src/Components/Translate.tsx | 10 ++++++++++ .../external-plugin-previewer/src/Components/index.tsx | 8 ++++++++ packages/external-plugin-previewer/src/DOMImpl.tsx | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 packages/external-plugin-previewer/src/Components/Translate.tsx diff --git a/packages/external-plugin-previewer/src/Components/MaskCard.tsx b/packages/external-plugin-previewer/src/Components/MaskCard.tsx index ab683d0703f0..f1b9982c5836 100644 --- a/packages/external-plugin-previewer/src/Components/MaskCard.tsx +++ b/packages/external-plugin-previewer/src/Components/MaskCard.tsx @@ -9,7 +9,7 @@ export const MaskCard: Component = (props) => { {String(props.caption)} - {String(props.title)} + diff --git a/packages/external-plugin-previewer/src/Components/Translate.tsx b/packages/external-plugin-previewer/src/Components/Translate.tsx new file mode 100644 index 000000000000..067e0630b4f6 --- /dev/null +++ b/packages/external-plugin-previewer/src/Components/Translate.tsx @@ -0,0 +1,10 @@ +import type { Component } from '.' + +export const Translate: Component<{}> = () => { + return ( + + i18n: + + ) +} +Translate.displayName = 'i18n-translate' diff --git a/packages/external-plugin-previewer/src/Components/index.tsx b/packages/external-plugin-previewer/src/Components/index.tsx index c7d82b836baf..8315c7ab6a1f 100644 --- a/packages/external-plugin-previewer/src/Components/index.tsx +++ b/packages/external-plugin-previewer/src/Components/index.tsx @@ -1,4 +1,12 @@ export { MaskCard } from './MaskCard' +export { Translate } from './Translate' + +export const Span: Component<{}> = () => ( + + + +) +Span.displayName = 'span' export interface Component

{ (props: P, dispatchEvent: (event: Event) => void): React.ReactChild displayName: string diff --git a/packages/external-plugin-previewer/src/DOMImpl.tsx b/packages/external-plugin-previewer/src/DOMImpl.tsx index 7e4092e6c454..948e3e6dbeb8 100644 --- a/packages/external-plugin-previewer/src/DOMImpl.tsx +++ b/packages/external-plugin-previewer/src/DOMImpl.tsx @@ -58,7 +58,7 @@ function render(f: Components.Component, props: any, shadow: ShadowRoot) { root.render(f(props, (event) => void shadow.host.dispatchEvent(event))) } -const unknown = ['div', (() => '') as any as Components.Component] as const +const unknown = ['div', (() => null) as any as Components.Component] as const function shouldRender(element: string): readonly [string, Components.Component] { for (const F of Object.values(Components)) { From fbe021a7ed3745de2bbf88b7c93b0935f42f3fea Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 19 May 2021 13:47:19 +0800 Subject: [PATCH 16/28] chore: change some custom dom --- .../src/Components/index.tsx | 25 +++++++++++++------ .../external-plugin-previewer/src/DOMImpl.tsx | 7 +++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/external-plugin-previewer/src/Components/index.tsx b/packages/external-plugin-previewer/src/Components/index.tsx index 8315c7ab6a1f..f3c0cacabe7e 100644 --- a/packages/external-plugin-previewer/src/Components/index.tsx +++ b/packages/external-plugin-previewer/src/Components/index.tsx @@ -1,13 +1,24 @@ +import { createElement } from 'react' + export { MaskCard } from './MaskCard' export { Translate } from './Translate' - -export const Span: Component<{}> = () => ( - - - -) -Span.displayName = 'span' export interface Component

{ (props: P, dispatchEvent: (event: Event) => void): React.ReactChild displayName: string } + +export const span = createNativeTagDelegate('span') +export const div = createNativeTagDelegate('div') +export const br = createNativeTagDelegate('br', { children: false }) +function createNativeTagDelegate( + tag: T, + accpetProps?: { [key in keyof HTMLElementTagNameMap[T]]?: boolean }, +) { + const C: Component<{}> = () => { + // TODO: implement acceptProps + if (accpetProps?.children === false) return createElement(tag) + return createElement(tag, {}, ) + } + C.displayName = tag + return C +} diff --git a/packages/external-plugin-previewer/src/DOMImpl.tsx b/packages/external-plugin-previewer/src/DOMImpl.tsx index 948e3e6dbeb8..9b5b0c2442cc 100644 --- a/packages/external-plugin-previewer/src/DOMImpl.tsx +++ b/packages/external-plugin-previewer/src/DOMImpl.tsx @@ -54,15 +54,16 @@ function createElement(element: string, options: ElementCreationOptions) { } function render(f: Components.Component, props: any, shadow: ShadowRoot) { - const root: ReactRootShadowed = (shadow as any).__root || ((shadow as any).__root = createReactRootShadowed(shadow)) + const root: ReactRootShadowed = + (shadow as any).__root || ((shadow as any).__root = createReactRootShadowed(shadow, { tag: 'span' })) root.render(f(props, (event) => void shadow.host.dispatchEvent(event))) } -const unknown = ['div', (() => null) as any as Components.Component] as const +const unknown = ['span', (() => null) as any as Components.Component] as const function shouldRender(element: string): readonly [string, Components.Component] { for (const F of Object.values(Components)) { - if (F.displayName === element) return ['main', F] + if (F.displayName === element) return ['span', F] } return unknown } From 0d3006ea9c89081624db31a355b1f7ac383c6d9c Mon Sep 17 00:00:00 2001 From: Jack Works Date: Wed, 19 May 2021 13:48:59 +0800 Subject: [PATCH 17/28] feat: a minimal permission management --- .../background-script/HelperService.ts | 16 ------- .../Jobs/InjectContentScripts.ts | 2 +- .../ThirdPartyPlugin/index.ts | 45 +++++++++++++++++++ .../ThirdPartyPlugin/types.ts | 11 +++++ .../src/extension/external-sdk/sdk.ts | 5 +++ .../popups/PermissionAwareRedirect/index.tsx | 2 +- .../maskbook/src/extension/popups/index.tsx | 2 +- packages/maskbook/src/extension/service.ts | 1 + .../components/ExternalPluginRenderer.tsx | 2 +- .../ShadowRoot/createReactRootShadowed.tsx | 7 ++- 10 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts create mode 100644 packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts diff --git a/packages/maskbook/src/extension/background-script/HelperService.ts b/packages/maskbook/src/extension/background-script/HelperService.ts index 3bc43b604570..8a04cc35dcdf 100644 --- a/packages/maskbook/src/extension/background-script/HelperService.ts +++ b/packages/maskbook/src/extension/background-script/HelperService.ts @@ -1,5 +1,4 @@ import { memoizePromise } from '../../utils/memoize' -import { getHostPermissionFieldFromURL } from '../popups/PermissionAwareRedirect/utils' import { constructRequestPermissionURL } from '../popups' const cache = new Map() @@ -42,21 +41,6 @@ export function saveAsFileFromBuffer(file: BufferSource, mimeType: string, fileN saveAsFileFromUrl(url, fileName) } -export function openDialogPopup(url: string) { - browser.windows.create({ - type: 'popup', - width: 400, - height: 600, - url: browser.runtime.getURL('/popups.html#' + url), - }) -} -export async function enableSDK(url: string) { - sessionStorage.setItem('sdk:' + getHostPermissionFieldFromURL(url), '1') -} -export function isSDKEnabled(url: string) { - return !!sessionStorage.getItem('sdk:' + getHostPermissionFieldFromURL(url)) -} - export async function requestBrowserPermission(permission: browser.permissions.Permissions) { if (await browser.permissions.contains(permission)) return true try { diff --git a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts index 2956d40fc38a..b4477ba696ea 100644 --- a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts +++ b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts @@ -1,5 +1,5 @@ import { Flags } from '../../../utils/flags' -import { isSDKEnabled } from '../HelperService' +import { isSDKEnabled } from '../ThirdPartyPlugin' type Args = browser.webNavigation.TransitionNavListener extends browser.webNavigation.NavListener ? U : never export default function () { diff --git a/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts new file mode 100644 index 000000000000..ee557831a542 --- /dev/null +++ b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts @@ -0,0 +1,45 @@ +import { ThirdPartyPluginPermission } from './types' + +export function openPluginPopup(url: string) { + new URL(url) // it must be a full qualified URL otherwise throws + browser.windows.create({ + type: 'popup', + width: 350, + height: 600, + url, + }) +} +export async function isSDKEnabled(baseURL: string) { + return hasPermission(baseURL, [ThirdPartyPluginPermission.SDKEnabled]) +} +export async function enableSDK(baseURL: string) { + return grantPermission(baseURL, ThirdPartyPluginPermission.SDKEnabled) +} +/** + * Check if the given URL has the permissions. + */ +export async function hasPermission(baseURL: string, permissions: ThirdPartyPluginPermission[]): Promise { + return permissions.every((p) => hasPermissionInternal(baseURL, p)) +} + +/** + * Request permission for the given URL. + * + */ +export async function requestPermission(baseURL: string, permissions: ThirdPartyPluginPermission[]): Promise { + return true +} + +/** + * DO NOT call this in the SDK. It should be called in the popups. + * + * Notice: In this demo implementation, all permissions are stored in the sessionStorage and will lost after the plugin refresh. + */ +export async function grantPermission(baseURL: string, permission: ThirdPartyPluginPermission) { + sessionStorage.setItem(`plugin:${ThirdPartyPluginPermission[permission]}:${baseURL}`, '1') +} + +/** @internal Do not export */ +function hasPermissionInternal(baseURL: string, permission: ThirdPartyPluginPermission) { + return !!sessionStorage.getItem(`plugin:${ThirdPartyPluginPermission[permission]}:${baseURL}`) +} diff --git a/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts new file mode 100644 index 000000000000..75e2f8369c43 --- /dev/null +++ b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts @@ -0,0 +1,11 @@ +export enum ThirdPartyPluginPermission { + /** + * This is the internal permission that used to indicate if we should inject + * sdk into a content script. + * + * This permission will be automatically granted when user interact with the plugin in the SNS. + * + * This permission should be revoked once the popup has closed. + */ + SDKEnabled, +} diff --git a/packages/maskbook/src/extension/external-sdk/sdk.ts b/packages/maskbook/src/extension/external-sdk/sdk.ts index daee19671967..40b9d15f753a 100644 --- a/packages/maskbook/src/extension/external-sdk/sdk.ts +++ b/packages/maskbook/src/extension/external-sdk/sdk.ts @@ -1,3 +1,5 @@ +import Services from '../service' + /** Version of this SDK */ export async function version() { return 1 @@ -5,3 +7,6 @@ export async function version() { export async function echo(x: T) { return x } +export async function getProfile() { + return (await Services.Identity.queryProfiles()).map((x) => x.identifier.userId) +} diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx index 962a9235d7b6..45363d514929 100644 --- a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx @@ -20,7 +20,7 @@ function Inner({ url }: { url: string }) { }, [url]) useEffect(() => { if (hasPermission) { - Services.Helper.enableSDK(url).then(() => (location.href = url)) + Services.ThirdPartyPlugin.enableSDK(url).then(() => (location.href = url)) } }, [hasPermission, url]) return ( diff --git a/packages/maskbook/src/extension/popups/index.tsx b/packages/maskbook/src/extension/popups/index.tsx index 58531dbced9b..754f98752fcb 100644 --- a/packages/maskbook/src/extension/popups/index.tsx +++ b/packages/maskbook/src/extension/popups/index.tsx @@ -7,6 +7,6 @@ export function getRouteURLWithNoParam(kind: DialogRoutes) { return browser.runtime.getURL(`/popups.html#${kind}`) } export function PermissionAwareRedirectOf(url: string) { - return `${DialogRoutes.PermissionAwareRedirect}?url=${encodeURIComponent(url)}` + return getRouteURLWithNoParam(DialogRoutes.PermissionAwareRedirect) + `?url=${encodeURIComponent(url)}` } export { constructRequestPermissionURL } from './RequestPermission/utils' diff --git a/packages/maskbook/src/extension/service.ts b/packages/maskbook/src/extension/service.ts index f23ecfb1e425..a765f828f663 100644 --- a/packages/maskbook/src/extension/service.ts +++ b/packages/maskbook/src/extension/service.ts @@ -33,6 +33,7 @@ export const Services = { Provider: add(() => import('./background-script/ProviderService'), 'Provider'), Ethereum: add(() => import('./background-script/EthereumService'), 'Ethereum'), Settings: add(() => import('./background-script/SettingsService'), 'Settings'), + ThirdPartyPlugin: add(() => import('./background-script/ThirdPartyPlugin'), 'ThirdPartyPlugin'), } export default Services export const ServicesWithProgress = add(() => import('./service-generator'), 'ServicesWithProgress', true) diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index f57fa29178e5..bb3e6bd6d870 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -7,7 +7,7 @@ import { PermissionAwareRedirectOf } from '../../../extension/popups' setHostConfig({ permissionAwareOpen(url) { - Services.Helper.openDialogPopup(PermissionAwareRedirectOf(url)) + Services.ThirdPartyPlugin.openPluginPopup(PermissionAwareRedirectOf(url)) }, }) diff --git a/packages/shared/src/ShadowRoot/createReactRootShadowed.tsx b/packages/shared/src/ShadowRoot/createReactRootShadowed.tsx index 7783736b5f98..2437dd66a2e8 100644 --- a/packages/shared/src/ShadowRoot/createReactRootShadowed.tsx +++ b/packages/shared/src/ShadowRoot/createReactRootShadowed.tsx @@ -9,6 +9,8 @@ import type {} from 'react-dom/experimental' import { ShadowRootStyleProvider } from './ShadowRootStyleProvider' export interface RenderInShadowRootConfig { + /** Root tag. @default "main" */ + tag?: string /** Allow to render multiple React root into a same ShadowRoot */ key?: string /** The AbortSignal to stop the render */ @@ -67,8 +69,9 @@ function mount( instanceConfig: RenderInShadowRootConfig, globalConfig: CreateRenderInShadowRootConfig, ): ReactRootShadowed { + const tag = instanceConfig.tag || 'main' const key = instanceConfig.key || 'main' - if (shadow.querySelector(`main.${key}`)) { + if (shadow.querySelector(`${tag}.${key}`)) { console.error('Tried to create root in', shadow, 'with key', key, ' which is already used. Skip rendering.') return { destory: () => {}, @@ -79,7 +82,7 @@ function mount( const wrap = globalConfig.wrapJSX jsx = getJSX(jsx) - const container = shadow.appendChild(document.createElement('main')) + const container = shadow.appendChild(document.createElement(tag)) container.className = key let undoActions: Function[] = [] From 690f3498bb42e6d3f1e594b95322fdc8f616976d Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 20 May 2021 16:54:42 +0800 Subject: [PATCH 18/28] feat: poor man's permission system --- .../ThirdPartyPlugin/index.ts | 37 ++++++++++++++++--- .../ThirdPartyPlugin/types.ts | 1 + .../src/extension/external-sdk/sdk.ts | 5 +++ .../popups/RequestPermission/index.tsx | 2 +- .../ThirdPartyRequestPermission.tsx | 22 +++++++++++ .../ThirdPartyRequestPermission/index.tsx | 24 ++++++++++++ .../ThirdPartyRequestPermission/utils.ts | 12 ++++++ .../maskbook/src/extension/popups/index.tsx | 1 + .../maskbook/src/extension/popups/render.tsx | 12 ++++-- packages/maskbook/src/extension/service.ts | 2 + .../components/ExternalPluginRenderer.tsx | 16 +------- 11 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/ThirdPartyRequestPermission.tsx create mode 100644 packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/index.tsx create mode 100644 packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/utils.ts diff --git a/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts index ee557831a542..5cf9473208bd 100644 --- a/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts +++ b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/index.ts @@ -1,19 +1,41 @@ +import type { Manifest } from '../../../plugins/External/types' +import { constructThirdPartyRequestPermissionURL } from '../../popups/ThirdPartyRequestPermission/utils' import { ThirdPartyPluginPermission } from './types' -export function openPluginPopup(url: string) { +export async function fetchManifest(addr: string): Promise { + const response = await fetch(addr + 'mask-manifest.json') + const json = await response.text().then(JSONC) + // TODO: verify manifest + return JSON.parse(json) + + function JSONC(x: string) { + return x + .split('\n') + .filter((x) => !x.match(/^ +\/\//)) + .join('\n') + } +} +export async function openPluginPopup(url: string) { new URL(url) // it must be a full qualified URL otherwise throws - browser.windows.create({ + const { id: windowID } = await browser.windows.create({ type: 'popup', width: 350, height: 600, url, }) + return new Promise((resolve) => { + browser.windows.onRemoved.addListener(function listener(id) { + if (id !== windowID) return + browser.windows.onRemoved.removeListener(listener) + resolve() + }) + }) } export async function isSDKEnabled(baseURL: string) { return hasPermission(baseURL, [ThirdPartyPluginPermission.SDKEnabled]) } export async function enableSDK(baseURL: string) { - return grantPermission(baseURL, ThirdPartyPluginPermission.SDKEnabled) + return grantPermission(baseURL, [ThirdPartyPluginPermission.SDKEnabled]) } /** * Check if the given URL has the permissions. @@ -27,7 +49,9 @@ export async function hasPermission(baseURL: string, permissions: ThirdPartyPlug * */ export async function requestPermission(baseURL: string, permissions: ThirdPartyPluginPermission[]): Promise { - return true + if (await hasPermission(baseURL, permissions)) return true + await openPluginPopup(constructThirdPartyRequestPermissionURL(baseURL, permissions)) + return hasPermission(baseURL, permissions) } /** @@ -35,8 +59,9 @@ export async function requestPermission(baseURL: string, permissions: ThirdParty * * Notice: In this demo implementation, all permissions are stored in the sessionStorage and will lost after the plugin refresh. */ -export async function grantPermission(baseURL: string, permission: ThirdPartyPluginPermission) { - sessionStorage.setItem(`plugin:${ThirdPartyPluginPermission[permission]}:${baseURL}`, '1') +export async function grantPermission(baseURL: string, permissions: ThirdPartyPluginPermission[]) { + for (const permission of permissions) + sessionStorage.setItem(`plugin:${ThirdPartyPluginPermission[permission]}:${baseURL}`, '1') } /** @internal Do not export */ diff --git a/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts index 75e2f8369c43..24eca7736bfe 100644 --- a/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts +++ b/packages/maskbook/src/extension/background-script/ThirdPartyPlugin/types.ts @@ -8,4 +8,5 @@ export enum ThirdPartyPluginPermission { * This permission should be revoked once the popup has closed. */ SDKEnabled, + DEBUG_Profiles, } diff --git a/packages/maskbook/src/extension/external-sdk/sdk.ts b/packages/maskbook/src/extension/external-sdk/sdk.ts index 40b9d15f753a..026a57f35391 100644 --- a/packages/maskbook/src/extension/external-sdk/sdk.ts +++ b/packages/maskbook/src/extension/external-sdk/sdk.ts @@ -1,3 +1,4 @@ +import { ThirdPartyPluginPermission } from '../background-script/ThirdPartyPlugin/types' import Services from '../service' /** Version of this SDK */ @@ -8,5 +9,9 @@ export async function echo(x: T) { return x } export async function getProfile() { + const granted = await Services.ThirdPartyPlugin.requestPermission(location.origin + '/', [ + ThirdPartyPluginPermission.DEBUG_Profiles, + ]) + if (!granted) throw new Error('Permission not granted') return (await Services.Identity.queryProfiles()).map((x) => x.identifier.userId) } diff --git a/packages/maskbook/src/extension/popups/RequestPermission/index.tsx b/packages/maskbook/src/extension/popups/RequestPermission/index.tsx index 805a43e0d22d..78a67a98c5a8 100644 --- a/packages/maskbook/src/extension/popups/RequestPermission/index.tsx +++ b/packages/maskbook/src/extension/popups/RequestPermission/index.tsx @@ -18,7 +18,7 @@ function isAcceptablePermission(x: string): x is browser.permissions.Permission return (acceptable as string[]).includes(x) } -export function RequestPermissionPage() { +export default function RequestPermissionPage() { const param = useLocation() const _ = new URLSearchParams(param.search) const origins = _.getAll('origins') diff --git a/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/ThirdPartyRequestPermission.tsx b/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/ThirdPartyRequestPermission.tsx new file mode 100644 index 000000000000..3caa8bf48423 --- /dev/null +++ b/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/ThirdPartyRequestPermission.tsx @@ -0,0 +1,22 @@ +import { ThirdPartyPluginPermission } from '../../background-script/ThirdPartyPlugin/types' + +export interface ThirdPartyRequestPermissionProps { + pluginURL: string + pluginName: string + permissions: ThirdPartyPluginPermission[] + onGrant(permissions: ThirdPartyPluginPermission[]): void +} +export function ThirdPartyRequestPermission(props: ThirdPartyRequestPermissionProps) { + return ( +

+ The plugin "{props.pluginName}" (hosted on {props.pluginURL}) is going to request the following permissions: +
    + {props.permissions.map((x) => ( +
  • {ThirdPartyPluginPermission[x]}
  • + ))} +
+ + +
+ ) +} diff --git a/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/index.tsx b/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/index.tsx new file mode 100644 index 000000000000..b35a8de1a0b6 --- /dev/null +++ b/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/index.tsx @@ -0,0 +1,24 @@ +import { useLocation } from 'react-router-dom' +import { useAsync } from 'react-use' +import Services from '../../service' +import { ThirdPartyRequestPermission } from './ThirdPartyRequestPermission' + +export default function () { + const param = useLocation() + const _ = new URLSearchParams(param.search) + const permission = _.getAll('permission') + const plugin = _.get('plugin') + const { value } = useAsync(() => Services.ThirdPartyPlugin.fetchManifest(plugin!), [plugin]) + if (!plugin) return null + if (!value) return null + return ( + parseInt(x, 10))} + onGrant={(granted) => { + Services.ThirdPartyPlugin.grantPermission(plugin, granted).then(() => window.close()) + }} + /> + ) +} diff --git a/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/utils.ts b/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/utils.ts new file mode 100644 index 000000000000..43fa6e1afc28 --- /dev/null +++ b/packages/maskbook/src/extension/popups/ThirdPartyRequestPermission/utils.ts @@ -0,0 +1,12 @@ +import { DialogRoutes, getRouteURLWithNoParam } from '..' +import type { ThirdPartyPluginPermission } from '../../background-script/ThirdPartyPlugin/types' + +export function constructThirdPartyRequestPermissionURL( + pluginManifestURL: string, + permissions: ThirdPartyPluginPermission[], +) { + const params = new URLSearchParams() + params.set('plugin', pluginManifestURL) + for (const x of permissions) params.append('permission', String(x)) + return getRouteURLWithNoParam(DialogRoutes.ThirdPartyRequestPermission) + '?' + params.toString() +} diff --git a/packages/maskbook/src/extension/popups/index.tsx b/packages/maskbook/src/extension/popups/index.tsx index 754f98752fcb..adf6d43d5249 100644 --- a/packages/maskbook/src/extension/popups/index.tsx +++ b/packages/maskbook/src/extension/popups/index.tsx @@ -1,6 +1,7 @@ export enum DialogRoutes { PermissionAwareRedirect = '/redirect', RequestPermission = '/request-permission', + ThirdPartyRequestPermission = '/3rd-request-permission', } export function getRouteURLWithNoParam(kind: DialogRoutes) { diff --git a/packages/maskbook/src/extension/popups/render.tsx b/packages/maskbook/src/extension/popups/render.tsx index 43c2ae400057..95fd88e2a916 100644 --- a/packages/maskbook/src/extension/popups/render.tsx +++ b/packages/maskbook/src/extension/popups/render.tsx @@ -7,22 +7,28 @@ import { HashRouter } from 'react-router-dom' import ReactDOM from 'react-dom' import { MaskUIRoot } from '../../UIRoot' import { DialogRoutes } from '.' -import { RequestPermissionPage } from './RequestPermission' const root = document.createElement('div') document.body.insertBefore(root, document.body.children[0] || null) ReactDOM.unstable_createRoot(root).render() +const RequestPermissionPage = lazy(() => import('./RequestPermission')) const PermissionAwareRedirect = lazy(() => import('./PermissionAwareRedirect')) +const ThirdPartyRequestPermission = lazy(() => import('./ThirdPartyRequestPermission')) function Dialogs() { return MaskUIRoot( - + - } exact /> + + + + + + , diff --git a/packages/maskbook/src/extension/service.ts b/packages/maskbook/src/extension/service.ts index a765f828f663..a6af8c079ce8 100644 --- a/packages/maskbook/src/extension/service.ts +++ b/packages/maskbook/src/extension/service.ts @@ -49,6 +49,8 @@ if (module.hot && isEnvironment(Environment.ManifestBackground)) { './background-script/HelperService', './background-script/ProviderService', './background-script/EthereumService', + './background-script/SettingsService', + './background-script/ThirdPartyPlugin', './service-generator', ], () => document.dispatchEvent(new Event(SERVICE_HMR_EVENT)), diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index bb3e6bd6d870..93959fde0620 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -54,7 +54,7 @@ export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { // TODO: support suspense function useExternalPluginManifest(url: string) { - return useAsyncRetry(() => fetchManifest(url), [url]) + return useAsyncRetry(() => Services.ThirdPartyPlugin.fetchManifest(url), [url]) } function useExternalPluginTemplate(url: string, manifest: Manifest | undefined, metaKey: string) { @@ -107,17 +107,3 @@ function htmlToTemplate(top: NodeListOf) { } return [...convertList(0, top)].join('\n') } - -async function fetchManifest(addr: string) { - const blob = await Services.Helper.fetch(addr + 'mask-manifest.json') - const json = await blob.text().then(JSONC) - // TODO: verify manifest - return JSON.parse(json) -} - -function JSONC(x: string) { - return x - .split('\n') - .filter((x) => !x.match(/^ +\/\//)) - .join('\n') -} From d5df272ab0130be29ab0a910a95d7d4eead64e86 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 27 May 2021 16:40:41 +0800 Subject: [PATCH 19/28] fix: lockfile --- pnpm-lock.yaml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e3fbf2efbd0..39ec4643766e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,7 +205,7 @@ importers: '@dimensiondev/maskbook-shared': link:../shared ef.js: 0.13.7 devDependencies: - snowpack: 3.3.5 + snowpack: 3.5.1 packages/icons: specifiers: @@ -14156,16 +14156,10 @@ packages: ci-info: 3.2.0 dev: true - /is-core-module/2.2.0: - resolution: {integrity: sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==} - dependencies: - has: 1.0.3 - /is-core-module/2.4.0: resolution: {integrity: sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==} dependencies: has: 1.0.3 - dev: true /is-data-descriptor/0.1.4: resolution: {integrity: sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=} @@ -19826,7 +19820,7 @@ packages: /resolve/1.20.0: resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} dependencies: - is-core-module: 2.2.0 + is-core-module: 2.4.0 path-parse: 1.0.6 /responselike/1.0.2: @@ -19886,7 +19880,7 @@ packages: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: - glob: 7.1.6 + glob: 7.1.7 dev: true /ripemd160/2.0.2: From 464d7779787abaa4e245475ff74e05080ac2defa Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 27 May 2021 17:32:50 +0800 Subject: [PATCH 20/28] chore: remove extra entry for sdk --- packages/maskbook/src/content-script.ts | 6 ++++++ .../background-script/Jobs/InjectContentScripts.ts | 5 ----- packages/maskbook/webpack.config.ts | 5 ----- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/maskbook/src/content-script.ts b/packages/maskbook/src/content-script.ts index 0783486495bb..bf337b1956c2 100644 --- a/packages/maskbook/src/content-script.ts +++ b/packages/maskbook/src/content-script.ts @@ -1,6 +1,12 @@ import './extension/content-script/hmr' +import Services from './extension/service' import { status } from './setup.ui' status.then((loaded) => { loaded && import('./extension/content-script/tasks') }) + +// The scope should be the ./ of the web page +Services.ThirdPartyPlugin.isSDKEnabled(new URL('./', location.href).href).then((result) => { + result && import('./extension/external-sdk') +}) diff --git a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts index b4477ba696ea..e8d307fd9153 100644 --- a/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts +++ b/packages/maskbook/src/extension/background-script/Jobs/InjectContentScripts.ts @@ -1,11 +1,9 @@ import { Flags } from '../../../utils/flags' -import { isSDKEnabled } from '../ThirdPartyPlugin' type Args = browser.webNavigation.TransitionNavListener extends browser.webNavigation.NavListener ? U : never export default function () { const injectedScript = fetchInjectedScript() const contentScripts = fetchInjectContentScript('/generated__content__script.html') - const sdk = fetchInjectContentScript('/generated__content__script__sdk.html') async function onCommittedListener(arg: Args): Promise { if (arg.url === 'about:blank') return if (!arg.url.startsWith('http')) return @@ -33,9 +31,6 @@ export default function () { }) } contentScripts(arg.tabId, arg.frameId).catch(HandleError(arg)) - if (isSDKEnabled(arg.url)) { - sdk(arg.tabId, arg.frameId).catch(HandleError(arg)) - } } browser.webNavigation.onCommitted.addListener(onCommittedListener) return () => browser.webNavigation.onCommitted.removeListener(onCommittedListener) diff --git a/packages/maskbook/webpack.config.ts b/packages/maskbook/webpack.config.ts index f2a5391fe71f..d531ad342d02 100644 --- a/packages/maskbook/webpack.config.ts +++ b/packages/maskbook/webpack.config.ts @@ -286,7 +286,6 @@ export default async function (cli_env: Record = {}, argv: { mo 'background-service': withBrowserPolyfill(src('./src/background-service.ts')), debug: withBrowserPolyfill(src('./src/extension/debug-page')), popups: withBrowserPolyfill(src('./src/extension/popups/render.tsx')), - 'content-script-external-sdk': withBrowserPolyfill(src('./src/extension/external-sdk')), } if (isManifestV3) delete main.entry['background-script'] if (mode === 'production') delete main.entry['dashboard-next'] @@ -299,10 +298,6 @@ export default async function (cli_env: Record = {}, argv: { mo getHTMLPlugin({ chunks: ['content-script'], filename: 'generated__content__script.html' }), getHTMLPlugin({ chunks: ['debug'], filename: 'debug.html' }), getHTMLPlugin({ chunks: ['popups'], filename: 'popups.html' }), - getHTMLPlugin({ - chunks: ['content-script-external-sdk'], - filename: 'generated__content__script__sdk.html', - }), ) // generate pages for each entry if (mode === 'development') main.plugins!.push(getHTMLPlugin({ chunks: ['dashboard-next'], filename: 'next.html' })) From d781410ae7e88ebc7af4cca9c1d86eacda831306 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Thu, 27 May 2021 17:41:15 +0800 Subject: [PATCH 21/28] fix: enable sdk --- .../src/extension/popups/PermissionAwareRedirect/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx index 45363d514929..2a919fc8b310 100644 --- a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx @@ -20,7 +20,7 @@ function Inner({ url }: { url: string }) { }, [url]) useEffect(() => { if (hasPermission) { - Services.ThirdPartyPlugin.enableSDK(url).then(() => (location.href = url)) + Services.ThirdPartyPlugin.enableSDK(new URL('./', url).href).then(() => (location.href = url)) } }, [hasPermission, url]) return ( From b95ea8f89b2bad08dc550d38ee35d5bea696230d Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 28 May 2021 14:28:27 +0800 Subject: [PATCH 22/28] feat: add sns context --- .../src/extension/external-sdk/constant.ts | 11 ++++++ .../src/extension/external-sdk/index.ts | 16 +++++---- .../src/extension/external-sdk/sdk.ts | 8 ++--- .../src/extension/external-sdk/sdk/context.ts | 34 +++++++++++++++++++ .../popups/PermissionAwareRedirect/index.tsx | 12 +++++-- .../maskbook/src/extension/popups/index.tsx | 9 +++-- .../components/ExternalPluginRenderer.tsx | 3 +- .../src/plugins/External/popup-context.ts | 28 +++++++++++++++ packages/maskbook/src/utils/messages.ts | 4 +++ 9 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 packages/maskbook/src/extension/external-sdk/constant.ts create mode 100644 packages/maskbook/src/extension/external-sdk/sdk/context.ts create mode 100644 packages/maskbook/src/plugins/External/popup-context.ts diff --git a/packages/maskbook/src/extension/external-sdk/constant.ts b/packages/maskbook/src/extension/external-sdk/constant.ts new file mode 100644 index 000000000000..8f5724b8aeda --- /dev/null +++ b/packages/maskbook/src/extension/external-sdk/constant.ts @@ -0,0 +1,11 @@ +import type { ThirdPartyPopupContextIdentifier } from '../../plugins/External/popup-context' + +export const currentPopupContext = new URL(location.href).searchParams.get( + 'mask_context', +) as ThirdPartyPopupContextIdentifier | null + +export enum SDKErrors { + M1_Lack_context_identifier = 'MaskErr/1: This page does not tied to any SNS context.', + M2_Context_disconnected = 'MaskErr/2: The SNS context associated with this page has gone.', + M3_Permission_denied = 'MaskErr/3: Permission not granted.', +} diff --git a/packages/maskbook/src/extension/external-sdk/index.ts b/packages/maskbook/src/extension/external-sdk/index.ts index 81f1053598cf..6bf81ce4fa4b 100644 --- a/packages/maskbook/src/extension/external-sdk/index.ts +++ b/packages/maskbook/src/extension/external-sdk/index.ts @@ -1,5 +1,5 @@ +import './constant' import { AsyncCall, JSONSerialization, EventBasedChannel } from 'async-call-rpc' -import SDK from './hmr-sdk' console.log('SDK server started') const channel: EventBasedChannel = { @@ -13,9 +13,13 @@ const channel: EventBasedChannel = { }, } -AsyncCall(SDK, { - serializer: JSONSerialization(undefined), - channel, - log: false, -}) +AsyncCall( + import('./hmr-sdk').then((x) => x.default), + { + serializer: JSONSerialization(undefined), + channel, + log: false, + }, +) document.dispatchEvent(new Event('mask-start')) +document.querySelector('html')?.setAttribute('data-mask-sdk-ready', 'true') diff --git a/packages/maskbook/src/extension/external-sdk/sdk.ts b/packages/maskbook/src/extension/external-sdk/sdk.ts index 026a57f35391..30095f5a256d 100644 --- a/packages/maskbook/src/extension/external-sdk/sdk.ts +++ b/packages/maskbook/src/extension/external-sdk/sdk.ts @@ -1,17 +1,17 @@ +// !! Change existing signature of anything this file exports leads to a breaking change. import { ThirdPartyPluginPermission } from '../background-script/ThirdPartyPlugin/types' import Services from '../service' +import { SDKErrors } from './constant' /** Version of this SDK */ export async function version() { return 1 } -export async function echo(x: T) { - return x -} +export { __assertLocalContext, __validateRemoteContext } from './sdk/context' export async function getProfile() { const granted = await Services.ThirdPartyPlugin.requestPermission(location.origin + '/', [ ThirdPartyPluginPermission.DEBUG_Profiles, ]) - if (!granted) throw new Error('Permission not granted') + if (!granted) throw new Error(SDKErrors.M3_Permission_denied) return (await Services.Identity.queryProfiles()).map((x) => x.identifier.userId) } diff --git a/packages/maskbook/src/extension/external-sdk/sdk/context.ts b/packages/maskbook/src/extension/external-sdk/sdk/context.ts new file mode 100644 index 000000000000..08b7c4f3b974 --- /dev/null +++ b/packages/maskbook/src/extension/external-sdk/sdk/context.ts @@ -0,0 +1,34 @@ +import { MaskMessage } from '../../../utils' +import { currentPopupContext, SDKErrors } from '../constant' + +/** @internal */ +export async function __assertLocalContext() { + if (!currentPopupContext) throw new Error(SDKErrors.M1_Lack_context_identifier) +} + +/** @internal To make sure the remote context still alive */ +export function __validateRemoteContext() { + if (isContextDisconnected) return Promise.reject(new Error(SDKErrors.M2_Context_disconnected)) + + return new Promise((resolve, reject) => { + if (!currentPopupContext) throw onContextDisconnected() + const challenge = Math.random() + const f = MaskMessage.events.thirdPartyPong.on((i) => { + if (i !== challenge) return + resolve() + f() + }) + MaskMessage.events.thirdPartyPing.sendToContentScripts({ + context: currentPopupContext, + challenge, + }) + setTimeout(() => reject(onContextDisconnected), 2000) + }) +} + +let isContextDisconnected = false +function onContextDisconnected() { + isContextDisconnected = true + document.dispatchEvent(new Event('mask-sdk-disconnected')) + return new Error(SDKErrors.M2_Context_disconnected) +} diff --git a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx index 2a919fc8b310..5ed6cb8d49d4 100644 --- a/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx +++ b/packages/maskbook/src/extension/popups/PermissionAwareRedirect/index.tsx @@ -9,18 +9,24 @@ import { PermissionAwareRedirectUI } from './ui' import { getHostPermissionFieldFromURL, isValidURL } from './utils' export default function PermissionAwareRedirect() { const url = new URLSearchParams(useLocation().search).get('url') + const context = new URLSearchParams(useLocation().search).get('context') if (!url) return + if (!context) return if (!isValidURL(url)) return - return + return } -function Inner({ url }: { url: string }) { +function Inner({ url, context }: { url: string; context: string }) { const { value: hasPermission, retry } = useAsyncRetry(async () => { if (!url) return false return browser.permissions.contains({ origins: [getHostPermissionFieldFromURL(url)] }) }, [url]) useEffect(() => { if (hasPermission) { - Services.ThirdPartyPlugin.enableSDK(new URL('./', url).href).then(() => (location.href = url)) + Services.ThirdPartyPlugin.enableSDK(new URL('./', url).href).then(() => { + const u = new URL(url) + u.searchParams.append('mask_context', context) + location.href = u.toString() + }) } }, [hasPermission, url]) return ( diff --git a/packages/maskbook/src/extension/popups/index.tsx b/packages/maskbook/src/extension/popups/index.tsx index adf6d43d5249..8a0596769d3e 100644 --- a/packages/maskbook/src/extension/popups/index.tsx +++ b/packages/maskbook/src/extension/popups/index.tsx @@ -1,3 +1,5 @@ +import type { ThirdPartyPopupContextIdentifier } from '../../plugins/External/popup-context' + export enum DialogRoutes { PermissionAwareRedirect = '/redirect', RequestPermission = '/request-permission', @@ -7,7 +9,10 @@ export enum DialogRoutes { export function getRouteURLWithNoParam(kind: DialogRoutes) { return browser.runtime.getURL(`/popups.html#${kind}`) } -export function PermissionAwareRedirectOf(url: string) { - return getRouteURLWithNoParam(DialogRoutes.PermissionAwareRedirect) + `?url=${encodeURIComponent(url)}` +export function PermissionAwareRedirectOf(url: string, context: ThirdPartyPopupContextIdentifier) { + return ( + getRouteURLWithNoParam(DialogRoutes.PermissionAwareRedirect) + + `?url=${encodeURIComponent(url)}&context=${context}` + ) } export { constructRequestPermissionURL } from './RequestPermission/utils' diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index 93959fde0620..c70eba5d2c28 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -4,10 +4,11 @@ import { useAsyncRetry } from 'react-use' import Services from '../../../extension/service' import { MaskExternalPluginPreviewRenderer, setHostConfig } from '@dimensiondev/external-plugin-previewer' import { PermissionAwareRedirectOf } from '../../../extension/popups' +import { createThirdPartyPopupContext } from '../popup-context' setHostConfig({ permissionAwareOpen(url) { - Services.ThirdPartyPlugin.openPluginPopup(PermissionAwareRedirectOf(url)) + Services.ThirdPartyPlugin.openPluginPopup(PermissionAwareRedirectOf(url, createThirdPartyPopupContext())) }, }) diff --git a/packages/maskbook/src/plugins/External/popup-context.ts b/packages/maskbook/src/plugins/External/popup-context.ts new file mode 100644 index 000000000000..ef9aa4b5ba37 --- /dev/null +++ b/packages/maskbook/src/plugins/External/popup-context.ts @@ -0,0 +1,28 @@ +import { MaskMessage, startEffect } from '../../utils' + +const bindingContext = new Set() +export type ThirdPartyPopupContextIdentifier = string & { __brand__: 'context' } +/** + * Generate a new random Third Party popup Context identifier. + * + * Most of the API calls in the Third Party popup need this identifier, + * otherwise we don't know where should we connect the context to. + * + * The context will be invalidated if this page is lost. + */ +export function createThirdPartyPopupContext(): ThirdPartyPopupContextIdentifier { + const id = Math.random().toString(16).slice(2) as ThirdPartyPopupContextIdentifier + bindingContext.add(id) + return id +} +function isLocalContext(x: string): x is ThirdPartyPopupContextIdentifier { + return bindingContext.has(x) +} + +startEffect(module.hot, () => { + return MaskMessage.events.thirdPartyPing.on((data) => { + if (isLocalContext(data.context)) { + MaskMessage.events.thirdPartyPong.sendToContentScripts(data.challenge) + } + }) +}) diff --git a/packages/maskbook/src/utils/messages.ts b/packages/maskbook/src/utils/messages.ts index a415b001ba45..d12c5eb51c28 100644 --- a/packages/maskbook/src/utils/messages.ts +++ b/packages/maskbook/src/utils/messages.ts @@ -2,6 +2,7 @@ import { WebExtensionMessage } from '@dimensiondev/holoflows-kit' import Serialization from './type-transform/Serialization' import type { ProfileIdentifier, GroupIdentifier, PersonaIdentifier } from '../database/type' import type { TypedMessage } from '../protocols/typed-message' +import type { ThirdPartyPopupContextIdentifier } from '../plugins/External/popup-context' export interface UpdateEvent { readonly reason: 'update' | 'delete' | 'new' @@ -49,6 +50,9 @@ export interface MaskMessages { before: PersonaIdentifier | undefined after: PersonaIdentifier | undefined }[] + // When a SNS page get this event, if it know this context, it should response the challenge with pong. + thirdPartyPing: { context: ThirdPartyPopupContextIdentifier; challenge: number } + thirdPartyPong: number } export const MaskMessage = new WebExtensionMessage({ domain: 'mask' }) Object.assign(globalThis, { MaskMessage }) From 9045d322aa4b9a8803d6fc5f6c34e8874e7c01e3 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 28 May 2021 15:55:48 +0800 Subject: [PATCH 23/28] feat: add entry for third party plugin --- .../src/plugins/External/SNSAdaptor/index.tsx | 2 + .../External/components/CompositionEntry.tsx | 19 ++++++ .../components/ExternalPluginRenderer.tsx | 60 +----------------- .../External/components/PluginLoader.tsx | 63 +++++++++++++++++++ .../src/plugins/External/loader/index.ts | 58 +++++++++++++++++ .../maskbook/src/plugins/External/types.tsx | 12 +++- 6 files changed, 154 insertions(+), 60 deletions(-) create mode 100644 packages/maskbook/src/plugins/External/components/CompositionEntry.tsx create mode 100644 packages/maskbook/src/plugins/External/components/PluginLoader.tsx create mode 100644 packages/maskbook/src/plugins/External/loader/index.ts diff --git a/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx b/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx index 913fd3b59e2f..96aa114836fa 100644 --- a/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx +++ b/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx @@ -3,6 +3,7 @@ import { Suspense } from 'react' import { ExternalPluginLoader } from '../components/Container' import type { ExternalPluginLoadDetails } from '../types' import { base } from '../base' +import { ThirdPartyPluginCompositionEntry } from '../components/CompositionEntry' const sns: Plugin.SNSAdaptor.Definition = { ...base, @@ -23,6 +24,7 @@ const sns: Plugin.SNSAdaptor.Definition = { ) }, + CompositionDialogEntry: { label: '🧩 Third party plugins', dialog: ThirdPartyPluginCompositionEntry }, } export default sns diff --git a/packages/maskbook/src/plugins/External/components/CompositionEntry.tsx b/packages/maskbook/src/plugins/External/components/CompositionEntry.tsx new file mode 100644 index 000000000000..17d905537a76 --- /dev/null +++ b/packages/maskbook/src/plugins/External/components/CompositionEntry.tsx @@ -0,0 +1,19 @@ +import type { Plugin } from '@dimensiondev/mask-plugin-infra/src' +import { usePortalShadowRoot } from '@dimensiondev/maskbook-shared' +import { MaskDialog } from '@dimensiondev/maskbook-theme' +import { DialogContent } from '@material-ui/core' +import { PluginLoader } from './PluginLoader' + +export function ThirdPartyPluginCompositionEntry(props: Plugin.SNSAdaptor.CompositionDialogEntry_DialogProps) { + return usePortalShadowRoot((container) => ( + + + + + + )) +} diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index c70eba5d2c28..db06d1470d92 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -1,10 +1,10 @@ -import type { ExternalPluginLoadDetails, Manifest } from '../types' +import type { ExternalPluginLoadDetails } from '../types' import { Card, CardHeader, CardContent, Typography, Link, Button } from '@material-ui/core' -import { useAsyncRetry } from 'react-use' import Services from '../../../extension/service' import { MaskExternalPluginPreviewRenderer, setHostConfig } from '@dimensiondev/external-plugin-previewer' import { PermissionAwareRedirectOf } from '../../../extension/popups' import { createThirdPartyPopupContext } from '../popup-context' +import { useExternalPluginManifest, useExternalPluginTemplate } from '../loader' setHostConfig({ permissionAwareOpen(url) { @@ -52,59 +52,3 @@ export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { ) } - -// TODO: support suspense -function useExternalPluginManifest(url: string) { - return useAsyncRetry(() => Services.ThirdPartyPlugin.fetchManifest(url), [url]) -} - -function useExternalPluginTemplate(url: string, manifest: Manifest | undefined, metaKey: string) { - const target = manifest?.metadata[metaKey] - // TODO: the final URL must inside/same directory of the manifest - const u = target ? new URL(target.preview, url).toString() : null - return useAsyncRetry(() => fetchTemplate(u), [u]) -} - -// TODO: support suspense -// TODO: cache -async function fetchTemplate(url: string | null) { - if (!url) return - const blob = await Services.Helper.fetch(url) - const text = await blob.text() - const parser = new DOMParser() - const dom = parser.parseFromString(text, 'text/html').querySelector('template')?.innerHTML - if (!dom) return null - const template = parser.parseFromString(dom, 'text/html').querySelector('body')?.childNodes - if (!template) return null - console.log(htmlToTemplate(template)) - return htmlToTemplate(template) -} - -const indentLevel = 2 -function htmlToTemplate(top: NodeListOf) { - function* text(indent: number, text: Text) { - for (const line of text.textContent?.split('\n') || '') { - yield `${getIndent(indent)}. ${line}` - } - } - function* html(indent: number, node: HTMLElement): Generator { - yield `${getIndent(indent)}>${node.tagName.toLowerCase()}` - for (const attr of node.attributes) { - yield `${getIndent(indent + indentLevel)}%${attr.name} = ${attr.value}` - } - yield* convertList(indent + indentLevel, node.childNodes) - } - function* convertList(indent: number, nodes: NodeListOf): Generator { - for (const node of nodes) { - if (isText(node)) yield* text(indent, node) - else if (node instanceof HTMLElement) yield* html(indent, node) - } - } - function getIndent(x: number) { - return ' '.repeat(x) - } - function isText(node: ChildNode): node is Text { - return node.nodeType === document.TEXT_NODE - } - return [...convertList(0, top)].join('\n') -} diff --git a/packages/maskbook/src/plugins/External/components/PluginLoader.tsx b/packages/maskbook/src/plugins/External/components/PluginLoader.tsx new file mode 100644 index 000000000000..847c39b6ac21 --- /dev/null +++ b/packages/maskbook/src/plugins/External/components/PluginLoader.tsx @@ -0,0 +1,63 @@ +import { TextField, Button, LinearProgress, SnackbarContent, Card, CardContent, Typography } from '@material-ui/core' +import { useState } from 'react' +import { Result } from 'ts-results' +import { PermissionAwareRedirectOf } from '../../../extension/popups' +import Services from '../../../extension/service' +import { useExternalPluginManifest } from '../loader' +import { createThirdPartyPopupContext } from '../popup-context' + +export function PluginLoader() { + const [input, setInput] = useState( + process.env.NODE_ENV === 'development' + ? 'http://localhost:4242/' + : 'http://dimensiondev.github.io/Mask-Plugin-Example/', + ) + const [url, setURL] = useState(null) + const invalidURL = Result.wrap(() => new URL(input)).err + return ( + <> + setInput(e.currentTarget.value)} + error={invalidURL} + helperText={invalidURL ? 'URL seems invalid' : undefined} + /> + + {url ? : null} + + ) +} + +function Loader(props: { url: string }) { + const load = useExternalPluginManifest(props.url) + if (load.loading) return + if (load.error || !load.value) return + const contribution = load.value.contribution?.composition + return ( + + + External plugin: {load.value.name} + Description: {load.value.description} + Publisher: {load.value.publisher} + + {contribution ? ( + + ) : null} + + + ) +} diff --git a/packages/maskbook/src/plugins/External/loader/index.ts b/packages/maskbook/src/plugins/External/loader/index.ts new file mode 100644 index 000000000000..31a43fc58cb0 --- /dev/null +++ b/packages/maskbook/src/plugins/External/loader/index.ts @@ -0,0 +1,58 @@ +import { useAsyncRetry } from 'react-use' +import Services from '../../../extension/service' +import type { Manifest } from '../types' + +// TODO: support suspense +export function useExternalPluginManifest(url: string) { + return useAsyncRetry(() => Services.ThirdPartyPlugin.fetchManifest(url), [url]) +} + +export function useExternalPluginTemplate(url: string, manifest: Manifest | undefined, metaKey: string) { + const target = manifest?.metadata?.[metaKey] + // TODO: the final URL must inside/same directory of the manifest + const u = target ? new URL(target.preview, url).toString() : null + return useAsyncRetry(() => fetchTemplate(u), [u]) +} + +// TODO: support suspense, cache +async function fetchTemplate(url: string | null) { + if (!url) return + const blob = await Services.Helper.fetch(url) + const text = await blob.text() + const parser = new DOMParser() + const dom = parser.parseFromString(text, 'text/html').querySelector('template')?.innerHTML + if (!dom) return null + const template = parser.parseFromString(dom, 'text/html').querySelector('body')?.childNodes + if (!template) return null + console.log(htmlToTemplate(template)) + return htmlToTemplate(template) +} + +const indentLevel = 2 +function htmlToTemplate(top: NodeListOf) { + function* text(indent: number, text: Text) { + for (const line of text.textContent?.split('\n') || '') { + yield `${getIndent(indent)}. ${line}` + } + } + function* html(indent: number, node: HTMLElement): Generator { + yield `${getIndent(indent)}>${node.tagName.toLowerCase()}` + for (const attr of node.attributes) { + yield `${getIndent(indent + indentLevel)}%${attr.name} = ${attr.value}` + } + yield* convertList(indent + indentLevel, node.childNodes) + } + function* convertList(indent: number, nodes: NodeListOf): Generator { + for (const node of nodes) { + if (isText(node)) yield* text(indent, node) + else if (node instanceof HTMLElement) yield* html(indent, node) + } + } + function getIndent(x: number) { + return ' '.repeat(x) + } + function isText(node: ChildNode): node is Text { + return node.nodeType === document.TEXT_NODE + } + return [...convertList(0, top)].join('\n') +} diff --git a/packages/maskbook/src/plugins/External/types.tsx b/packages/maskbook/src/plugins/External/types.tsx index 6a2c00d8e9d6..bffeebfd4850 100644 --- a/packages/maskbook/src/plugins/External/types.tsx +++ b/packages/maskbook/src/plugins/External/types.tsx @@ -9,7 +9,15 @@ export interface ExternalPluginLoadDetails { export interface Manifest { manifest_version: 0 name: string - description: string + description?: string publisher: string - metadata: Record + metadata?: Record + contribution?: Manifest_Contribution +} +export interface Manifest_Contribution { + composition?: Manifest_Contribution_Composition +} +export interface Manifest_Contribution_Composition { + icon?: string + href: string } From ee5e59627b1c4a131b204d9cbc06aee675299294 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 28 May 2021 17:12:08 +0800 Subject: [PATCH 24/28] feat: add setMetadata --- .../src/extension/external-sdk/constant.ts | 2 ++ .../src/extension/external-sdk/sdk.ts | 20 +++++++++++++- .../src/extension/external-sdk/sdk/context.ts | 7 ++--- .../src/plugins/External/popup-context.ts | 13 ++------- .../src/plugins/External/reactions.ts | 27 +++++++++++++++++++ packages/maskbook/src/utils/messages.ts | 5 ++++ 6 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 packages/maskbook/src/plugins/External/reactions.ts diff --git a/packages/maskbook/src/extension/external-sdk/constant.ts b/packages/maskbook/src/extension/external-sdk/constant.ts index 8f5724b8aeda..bad5f563dd79 100644 --- a/packages/maskbook/src/extension/external-sdk/constant.ts +++ b/packages/maskbook/src/extension/external-sdk/constant.ts @@ -4,6 +4,8 @@ export const currentPopupContext = new URL(location.href).searchParams.get( 'mask_context', ) as ThirdPartyPopupContextIdentifier | null +export const currentBaseURL = new URL('./', location.href).toString() + export enum SDKErrors { M1_Lack_context_identifier = 'MaskErr/1: This page does not tied to any SNS context.', M2_Context_disconnected = 'MaskErr/2: The SNS context associated with this page has gone.', diff --git a/packages/maskbook/src/extension/external-sdk/sdk.ts b/packages/maskbook/src/extension/external-sdk/sdk.ts index 30095f5a256d..4db8f1b8e2c5 100644 --- a/packages/maskbook/src/extension/external-sdk/sdk.ts +++ b/packages/maskbook/src/extension/external-sdk/sdk.ts @@ -1,7 +1,9 @@ // !! Change existing signature of anything this file exports leads to a breaking change. +import { MaskMessage } from '../../utils' import { ThirdPartyPluginPermission } from '../background-script/ThirdPartyPlugin/types' import Services from '../service' -import { SDKErrors } from './constant' +import { currentBaseURL, SDKErrors } from './constant' +import { __validateRemoteContext } from './sdk/context' /** Version of this SDK */ export async function version() { @@ -15,3 +17,19 @@ export async function getProfile() { if (!granted) throw new Error(SDKErrors.M3_Permission_denied) return (await Services.Identity.queryProfiles()).map((x) => x.identifier.userId) } +export async function setPayload(payload: Record, options: { additionText: string }) { + const context = await __validateRemoteContext() + + const url = currentBaseURL.replace(/^https?:\/\//, '') + const namespacedPayload: Record = {} + for (const key in payload) { + // plugin:dimensiondev.github.io/Mask-Plugin-Example/@v1 + namespacedPayload[`plugin:${url}@${key}`] = payload[key] + } + + MaskMessage.events.thirdPartySetPayload.sendToContentScripts({ + payload: namespacedPayload, + context, + appendText: options.additionText, + }) +} diff --git a/packages/maskbook/src/extension/external-sdk/sdk/context.ts b/packages/maskbook/src/extension/external-sdk/sdk/context.ts index 08b7c4f3b974..0cf41b46de20 100644 --- a/packages/maskbook/src/extension/external-sdk/sdk/context.ts +++ b/packages/maskbook/src/extension/external-sdk/sdk/context.ts @@ -1,3 +1,4 @@ +import type { ThirdPartyPopupContextIdentifier } from '../../../plugins/External/popup-context' import { MaskMessage } from '../../../utils' import { currentPopupContext, SDKErrors } from '../constant' @@ -10,19 +11,19 @@ export async function __assertLocalContext() { export function __validateRemoteContext() { if (isContextDisconnected) return Promise.reject(new Error(SDKErrors.M2_Context_disconnected)) - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (!currentPopupContext) throw onContextDisconnected() const challenge = Math.random() const f = MaskMessage.events.thirdPartyPong.on((i) => { if (i !== challenge) return - resolve() + resolve(currentPopupContext!) f() }) MaskMessage.events.thirdPartyPing.sendToContentScripts({ context: currentPopupContext, challenge, }) - setTimeout(() => reject(onContextDisconnected), 2000) + setTimeout(() => reject(onContextDisconnected()), 2000) }) } diff --git a/packages/maskbook/src/plugins/External/popup-context.ts b/packages/maskbook/src/plugins/External/popup-context.ts index ef9aa4b5ba37..7685df43934b 100644 --- a/packages/maskbook/src/plugins/External/popup-context.ts +++ b/packages/maskbook/src/plugins/External/popup-context.ts @@ -1,5 +1,4 @@ -import { MaskMessage, startEffect } from '../../utils' - +import './reactions' const bindingContext = new Set() export type ThirdPartyPopupContextIdentifier = string & { __brand__: 'context' } /** @@ -15,14 +14,6 @@ export function createThirdPartyPopupContext(): ThirdPartyPopupContextIdentifier bindingContext.add(id) return id } -function isLocalContext(x: string): x is ThirdPartyPopupContextIdentifier { +export function isLocalContext(x: string): x is ThirdPartyPopupContextIdentifier { return bindingContext.has(x) } - -startEffect(module.hot, () => { - return MaskMessage.events.thirdPartyPing.on((data) => { - if (isLocalContext(data.context)) { - MaskMessage.events.thirdPartyPong.sendToContentScripts(data.challenge) - } - }) -}) diff --git a/packages/maskbook/src/plugins/External/reactions.ts b/packages/maskbook/src/plugins/External/reactions.ts new file mode 100644 index 000000000000..c98885e0edd1 --- /dev/null +++ b/packages/maskbook/src/plugins/External/reactions.ts @@ -0,0 +1,27 @@ +import { makeTypedMessageText } from '../../protocols/typed-message' +import { editActivatedPostMetadata } from '../../protocols/typed-message/global-state' +import { MaskMessage, startEffect } from '../../utils' +import { isLocalContext } from './popup-context' + +startEffect(module.hot, () => + MaskMessage.events.thirdPartyPing.on((data) => { + if (!isLocalContext(data.context)) return + MaskMessage.events.thirdPartyPong.sendToContentScripts(data.challenge) + }), +) + +startEffect(module.hot, () => + MaskMessage.events.thirdPartySetPayload.on((data) => { + if (!isLocalContext(data.context)) return + editActivatedPostMetadata((meta) => { + for (const [key, value] of Object.entries(data.payload)) { + meta.set(key, value) + } + }) + MaskMessage.events.compositionUpdated.sendToLocal({ + open: true, + reason: 'popup', + content: makeTypedMessageText(data.appendText), + }) + }), +) diff --git a/packages/maskbook/src/utils/messages.ts b/packages/maskbook/src/utils/messages.ts index d12c5eb51c28..d4d417eaf69d 100644 --- a/packages/maskbook/src/utils/messages.ts +++ b/packages/maskbook/src/utils/messages.ts @@ -53,6 +53,11 @@ export interface MaskMessages { // When a SNS page get this event, if it know this context, it should response the challenge with pong. thirdPartyPing: { context: ThirdPartyPopupContextIdentifier; challenge: number } thirdPartyPong: number + thirdPartySetPayload: { + payload: Record + appendText: string + context: ThirdPartyPopupContextIdentifier + } } export const MaskMessage = new WebExtensionMessage({ domain: 'mask' }) Object.assign(globalThis, { MaskMessage }) From 25d3749159a80f83f7f40fb701ec54a8ed62be54 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 28 May 2021 18:22:52 +0800 Subject: [PATCH 25/28] feat: metadata badge --- .../InjectedComponents/PostDialog.tsx | 83 +++++++++++++++---- packages/plugin-infra/src/types.ts | 17 ++++ 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/packages/maskbook/src/components/InjectedComponents/PostDialog.tsx b/packages/maskbook/src/components/InjectedComponents/PostDialog.tsx index 39f2f9f91291..327efc648591 100644 --- a/packages/maskbook/src/components/InjectedComponents/PostDialog.tsx +++ b/packages/maskbook/src/components/InjectedComponents/PostDialog.tsx @@ -100,26 +100,15 @@ export function PostDialogUI(props: PostDialogUIProps) { } if (!isTypedMessageText(props.postContent)) return <>Unsupported type to edit - const metadataBadge = [...PluginUI].flatMap((plugin) => + const oldMetadataBadge = [...PluginUI].flatMap((plugin) => Result.wrap(() => { const knownMeta = plugin.postDialogMetadataBadge if (!knownMeta) return undefined return [...knownMeta.entries()].map(([metadataKey, tag]) => { return renderWithMetadataUntyped(props.postContent.meta, metadataKey, (r) => ( - - - editActivatedPostMetadata((meta) => meta.delete(metadataKey))} - label={tag(r)} - /> - - + + {tag(r)} + )) }) }).unwrapOr(null), @@ -150,7 +139,8 @@ export function PostDialogUI(props: PostDialogUIProps) { - {metadataBadge} + + {oldMetadataBadge} {result} } +function BadgeRenderer({ meta }: { meta: TypedMessage['meta'] }) { + const plugins = useActivatedPluginsSNSAdaptor() + if (!meta) return null + const metadata = [...meta.entries()] + return ( + <> + {metadata.flatMap(([key, value]) => { + return plugins.map((plugin) => { + const render = plugin.CompositionDialogMetadataBadgeRender + if (!render) return null + + if (typeof render === 'function') { + if (process.env.NODE_ENV === 'development') + return normalizeBadgeDescriptor(key, plugin, render(key, value)) + try { + return normalizeBadgeDescriptor(key, plugin, render(key, value)) + } catch (e) { + console.error(e) + return null + } + } else { + const f = render.get(key) + if (!f) return null + if (process.env.NODE_ENV === 'development') + return normalizeBadgeDescriptor(key, plugin, f(value)) + try { + return normalizeBadgeDescriptor(key, plugin, f(value)) + } catch (e) { + console.error(e) + return null + } + } + }) + })} + + ) +} +function normalizeBadgeDescriptor( + meta: string, + plugin: Plugin.SNSAdaptor.Definition, + desc: Plugin.SNSAdaptor.BadgeDescriptor | string | null, +) { + if (!desc) return null + if (typeof desc === 'string') desc = { text: desc, tooltip: `Provided by plugin "${plugin.name.fallback}"` } + return ( + + {desc.text} + + ) +} +function MetaBadge({ title, children, meta: key }: React.PropsWithChildren<{ title: React.ReactChild; meta: string }>) { + return ( + + + + editActivatedPostMetadata((meta) => meta.delete(key))} label={children} /> + + + + ) +} function renderLabel(label: Plugin.SNSAdaptor.CompositionDialogEntry['label']): React.ReactNode { if (!label) return null if (typeof label === 'object' && 'fallback' in label) return label.fallback diff --git a/packages/plugin-infra/src/types.ts b/packages/plugin-infra/src/types.ts index fd1b2455e39a..be556ec87c28 100644 --- a/packages/plugin-infra/src/types.ts +++ b/packages/plugin-infra/src/types.ts @@ -161,6 +161,8 @@ export namespace Plugin.SNSAdaptor { GlobalInjection?: InjectUI<{}> /** This UI will be an entry to the plugin in the Composition dialog of Mask. */ CompositionDialogEntry?: CompositionDialogEntry + /** This UI will be use when there is known badges. */ + CompositionDialogMetadataBadgeRender?: CompositionMetadataBadgeRender } /** * The entry has two type: @@ -197,6 +199,21 @@ export namespace Plugin.SNSAdaptor { open: boolean onClose(): void } + export type CompositionMetadataBadgeRender = + | CompositionMetadataBadgeRenderStatic + | CompositionMetadataBadgeRenderDynamic + export type CompositionMetadataBadgeRenderStatic = ReadonlyMap + export type CompositionMetadataBadgeRenderStaticMapper = ( + metadata: T, + ) => string | BadgeDescriptor | null + export type CompositionMetadataBadgeRenderDynamic = ( + key: string, + metadata: unknown, + ) => string | BadgeDescriptor | null + export interface BadgeDescriptor { + text: string | React.ReactChild + tooltip?: React.ReactChild + } } /** This part runs in the dashboard */ From 5f5cd493c2f3f33b3ec5999b8aa107d3d4b13b08 Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 28 May 2021 18:23:50 +0800 Subject: [PATCH 26/28] feat: metadata badge of 3rd plugin --- .../maskbook/src/plugins/External/SNSAdaptor/index.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx b/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx index 96aa114836fa..36d34e11b196 100644 --- a/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx +++ b/packages/maskbook/src/plugins/External/SNSAdaptor/index.tsx @@ -25,6 +25,13 @@ const sns: Plugin.SNSAdaptor.Definition = { ) }, CompositionDialogEntry: { label: '🧩 Third party plugins', dialog: ThirdPartyPluginCompositionEntry }, + CompositionDialogMetadataBadgeRender: (key, val) => + key.startsWith('plugin:') + ? { + text: `A 3rd party plugin (${key})`, + tooltip: `It's inner content: ${JSON.stringify(val)}`, + } + : null, } export default sns From 1cd9d7b0595e5300a4c609aaa46b6d8d0f34cc3e Mon Sep 17 00:00:00 2001 From: Jack Works Date: Fri, 28 May 2021 19:13:55 +0800 Subject: [PATCH 27/28] feat: allow relative path --- .../src/Components/MaskCard.tsx | 21 +++++++++++++++++-- .../src/Components/index.tsx | 1 + .../external-plugin-previewer/src/DOMImpl.tsx | 6 +++++- .../external-plugin-previewer/src/index.tsx | 6 ++++-- .../components/ExternalPluginRenderer.tsx | 1 + 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/external-plugin-previewer/src/Components/MaskCard.tsx b/packages/external-plugin-previewer/src/Components/MaskCard.tsx index f1b9982c5836..b99641720857 100644 --- a/packages/external-plugin-previewer/src/Components/MaskCard.tsx +++ b/packages/external-plugin-previewer/src/Components/MaskCard.tsx @@ -1,9 +1,11 @@ import { Card, CardContent, Typography, CardActions, Button } from '@material-ui/core' import { hostConfig } from '../host' import type { Component } from './index' +import { useRef } from 'react' export const MaskCard: Component = (props) => { + const ref = useRef(null) return ( - + {String(props.caption)} @@ -16,7 +18,13 @@ export const MaskCard: Component = (props) => { - @@ -30,3 +38,12 @@ export interface MaskCardProps { button: string href: string } +function getContext(node: Node | ShadowRoot | null): string | null { + if (!node) return null + if (node instanceof Element && node.hasAttribute('data-plugin')) { + return node.getAttribute('data-plugin') + } + if (node instanceof ShadowRoot) return getContext(node.host) + if (node.parentNode) return getContext(node.parentNode) + return null +} diff --git a/packages/external-plugin-previewer/src/Components/index.tsx b/packages/external-plugin-previewer/src/Components/index.tsx index f3c0cacabe7e..26e3f3c900ff 100644 --- a/packages/external-plugin-previewer/src/Components/index.tsx +++ b/packages/external-plugin-previewer/src/Components/index.tsx @@ -2,6 +2,7 @@ import { createElement } from 'react' export { MaskCard } from './MaskCard' export { Translate } from './Translate' + export interface Component

{ (props: P, dispatchEvent: (event: Event) => void): React.ReactChild displayName: string diff --git a/packages/external-plugin-previewer/src/DOMImpl.tsx b/packages/external-plugin-previewer/src/DOMImpl.tsx index 9b5b0c2442cc..96e4e5ba820b 100644 --- a/packages/external-plugin-previewer/src/DOMImpl.tsx +++ b/packages/external-plugin-previewer/src/DOMImpl.tsx @@ -56,7 +56,11 @@ function createElement(element: string, options: ElementCreationOptions) { function render(f: Components.Component, props: any, shadow: ShadowRoot) { const root: ReactRootShadowed = (shadow as any).__root || ((shadow as any).__root = createReactRootShadowed(shadow, { tag: 'span' })) - root.render(f(props, (event) => void shadow.host.dispatchEvent(event))) + root.render( f(props, (event) => void shadow.host.dispatchEvent(event))} />) +} +// Need use a JSX component to hold hooks +function HooksContainer(props: { f: () => React.ReactNode }) { + return <>{props.f()} } const unknown = ['span', (() => null) as any as Components.Component] as const diff --git a/packages/external-plugin-previewer/src/index.tsx b/packages/external-plugin-previewer/src/index.tsx index 18f935c696cd..05ced39cd16d 100644 --- a/packages/external-plugin-previewer/src/index.tsx +++ b/packages/external-plugin-previewer/src/index.tsx @@ -4,10 +4,11 @@ export type { HostConfig } from './host' import { useEffect, useState } from 'react' import { create } from 'ef.js' import './DOMImpl' -export function MaskExternalPluginPreviewRenderer({ payload, script, template, onError }: RenderData) { +export function MaskExternalPluginPreviewRenderer({ pluginBase, payload, script, template, onError }: RenderData) { const [dom, setDOM] = useState(null) useEffect(() => { if (!dom) return + dom.setAttribute('data-plugin', pluginBase) // This is safe. ef template does not allow any form of dynamic code execute in the template. try { const RemoteContent = create(template) @@ -18,10 +19,11 @@ export function MaskExternalPluginPreviewRenderer({ payload, script, template, o onError?.(e) } return - }, [dom, onError, payload, template]) + }, [dom, onError, payload, template, pluginBase]) return

setDOM(ref)} /> } export interface RenderData { + pluginBase: string template: string /** Currently not supported */ script: string diff --git a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx index db06d1470d92..9bfc2959449e 100644 --- a/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx +++ b/packages/maskbook/src/plugins/External/components/ExternalPluginRenderer.tsx @@ -43,6 +43,7 @@ export function ExternalPluginRenderer(props: ExternalPluginLoadDetails) { /> Date: Fri, 28 May 2021 20:05:35 +0800 Subject: [PATCH 28/28] fix: url bug --- packages/maskbook/src/extension/external-sdk/sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/maskbook/src/extension/external-sdk/sdk.ts b/packages/maskbook/src/extension/external-sdk/sdk.ts index 4db8f1b8e2c5..c020ca64a704 100644 --- a/packages/maskbook/src/extension/external-sdk/sdk.ts +++ b/packages/maskbook/src/extension/external-sdk/sdk.ts @@ -11,7 +11,7 @@ export async function version() { } export { __assertLocalContext, __validateRemoteContext } from './sdk/context' export async function getProfile() { - const granted = await Services.ThirdPartyPlugin.requestPermission(location.origin + '/', [ + const granted = await Services.ThirdPartyPlugin.requestPermission(new URL('./', location.href).toString(), [ ThirdPartyPluginPermission.DEBUG_Profiles, ]) if (!granted) throw new Error(SDKErrors.M3_Permission_denied)