// Copyright (C) 2023 Vaughn Nugent
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import { runtime } from "webextension-polyfill";
import { serializeError, deserializeError } from 'serialize-error';
import { JsonObject } from "type-fest";
import { cloneDeep, isArray, isObjectLike, set } from "lodash";
import { debugLog } from "@vnuge/vnlib.browser";
import { ChannelContext, createMessageChannel } from "../../messaging";
export interface BgRuntime {
readonly state: T;
onInstalled(callback: () => Promise): void;
onConnected(callback: () => Promise): void;
}
export type FeatureApi = {
[key: string]: (... args: any[]) => Promise
};
export type SendMessageHandler = (action: string, data: any) => Promise
export type VarArgsFunction = (...args: any[]) => T
export type FeatureConstructor = () => IFeatureExport
export type DummyApiExport = Array
export interface IFeatureExport {
/**
* Initializes a feature for mapping in the background runtime context
* @param bgRuntime The background runtime context
* @returns The feature's background api handlers that maps to the foreground
*/
background(bgRuntime: BgRuntime): TFeature
/**
* Initializes the feature for mapping in any foreground runtime context
* @returns The feature's foreground api stub methods for mapping. They must
* match the background api
*/
foreground(): TFeature
}
export interface IForegroundUnwrapper {
/**
* Unwraps a foreground feature and builds it's method bindings to
* the background handler
* @param feature The foreground feature that will be mapped to it's
* background handlers
* @returns The foreground feature's api stub methods
*/
use: (feature: FeatureConstructor) => T
}
export interface IBackgroundWrapper {
register(features: FeatureConstructor[]): void
}
export interface ProtectedFunction extends Function {
readonly protection: ChannelContext[]
}
export const optionsOnly = (func: T): T => protectMethod(func, 'options');
export const popupOnly = (func: T): T => protectMethod(func, 'popup');
export const contentScriptOnly = (func: T): T => protectMethod(func, 'content-script');
export const popupAndOptionsOnly = (func: T): T => protectMethod(func, 'popup', 'options');
export const protectMethod = (func: T, ...protection: ChannelContext[]): T => {
(func as any).protection = protection
return func;
}
/**
* Creates a background runtime context for registering background
* script feature api handlers
*/
export const useBackgroundFeatures = (state: TState): IBackgroundWrapper => {
const { openOnMessageChannel } = createMessageChannel('background');
const { onMessage } = openOnMessageChannel()
const rt = {
state,
onConnected: runtime.onConnect.addListener,
onInstalled: runtime.onInstalled.addListener,
} as BgRuntime
/**
* Each plugin will export named methods. Background methods
* are captured and registered as on-message handlers that
* correspond to the method name. Foreground method calls
* are redirected to the send-message of the same unique name
*/
return{
register: (features: FeatureConstructor[]) => {
//Loop through features
for (const feature of features) {
//Init feature
const f = feature().background(rt)
//Get all exported function
for (const externFuncName in f) {
//get exported function
const func = f[externFuncName] as Function
const onMessageFuncName = `${feature.name}-${externFuncName}`
//register method with api
onMessage(onMessageFuncName, async (sender, payload) => {
try {
if ((func as ProtectedFunction).protection
&& !(func as ProtectedFunction).protection.includes(sender)) {
throw new Error(`Unauthorized external call to ${onMessageFuncName}`)
}
const res = await func(...payload)
if(isArray(res)){
return [...res]
}
else if(isObjectLike(res)){
return { ...res }
}
else{
return res
}
}
catch (e: any) {
debugLog(`Error in method ${onMessageFuncName}`, e)
const s = serializeError(e)
return {
bridgeMessageException: JSON.stringify(s),
axiosResponseError: JSON.stringify(e.response)
}
}
});
}
}
}
}
}
/**
* Creates a foreground runtime context for unwrapping foreground stub
* methods and redirecting them to thier background handler
*/
export const useForegoundFeatures = (context: ChannelContext): IForegroundUnwrapper => {
const { openChannel } = createMessageChannel(context);
const { sendMessage } = openChannel()
/**
* The goal of this function is to get the foreground interface object
* that should match the background implementation. All methods are
* intercepted and redirected to the background via send-message
*/
return{
use: (feature:FeatureConstructor): T => {
//Register the feature
const api = feature().foreground()
const featureName = feature.name
const proxied : T = {} as T
//Loop through all methods
for(const funcName in api){
//Create proxy for each method
set(proxied, funcName, async (...args:any) => {
//Check for exceptions
const result = await sendMessage(`${featureName}-${funcName}`, cloneDeep(args)) as any
if(result?.bridgeMessageException){
const str = JSON.parse(result.bridgeMessageException)
const err = deserializeError(str)
//Recover axios response
if(result.axiosResponseError){
(err as any).response = JSON.parse(result.axiosResponseError)
}
throw err;
}
return result;
})
}
return proxied;
}
}
}
export const exportForegroundApi = (args: DummyApiExport): () => T => {
//Create the type from the array of type properties
const type = {} as T
//Loop through all properties
for(const prop of args){
//Default the property to an implementation error
type[prop] = (async () => {
throw new Error(`Method ${prop.toString()} not implemented`)
}) as any
}
return () => type
}