// 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 { storage, tabs, type Tabs } from "webextension-polyfill";
import { Watchable, useSingleSlotStorage } from "./types";
import { defaultTo, filter, includes, isEqual } from "lodash";
import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from "./framework";
import { AppSettings } from "./settings";
import { set, get, watchOnce, useToggle } from "@vueuse/core";
import { computed, ref } from "vue";
interface AllowedSites{
origins: string[];
enabled: boolean;
}
export interface AllowedOriginStatus{
readonly allowedOrigins: string[];
readonly enabled: boolean;
readonly currentOrigin?: string;
readonly isAllowed: boolean;
}
export interface InjectAllowlistApi extends FeatureApi, Watchable {
addOrigin(origin?: string): Promise;
removeOrigin(origin?: string): Promise;
getStatus(): Promise;
enable(value: boolean): Promise;
}
export const useInjectAllowList = (): IFeatureExport => {
return {
background: ({ }: BgRuntime) => {
const store = useSingleSlotStorage(storage.local, 'nip07-allowlist', { origins: [], enabled: true });
//watch current tab
const allowedOrigins = ref([])
const protectionEnabled = ref(true)
const [manullyTriggered, trigger] = useToggle()
const { currentOrigin, currentTab } = (() => {
const currentTab = ref(undefined)
const currentOrigin = computed(() => currentTab.value?.url ? new URL(currentTab.value.url).origin : undefined)
//Watch for changes to the current tab
tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
//If the url changed, update the current tab
if (changeInfo.url) {
currentTab.value = tab
}
})
tabs.onActivated.addListener(async ({ tabId }) => {
//Get the tab
const tab = await tabs.get(tabId)
//Update the current tab
currentTab.value = tab
})
return { currentTab, currentOrigin }
})()
const writeChanges = async () => {
await store.set({ origins: [...get(allowedOrigins)], enabled: get(protectionEnabled) })
}
//Initial load
store.get().then((data) => {
allowedOrigins.value = data.origins
protectionEnabled.value = data.enabled
})
const isOriginAllowed = (origin?: string): boolean => {
//If protection is not enabled, allow all
if(protectionEnabled.value == false){
return true;
}
//if no origin specified, use current origin
origin = defaultTo(origin, currentOrigin.value)
//If no origin, return false
if (!origin) {
return false;
}
//Default to origin only
const originOnly = new URL(origin).origin
return includes(allowedOrigins.value, originOnly)
}
const addOrigin = async (origin?: string): Promise => {
//if no origin specified, use current origin
const newOrigin = defaultTo(origin, currentOrigin.value)
if (!newOrigin) {
return;
}
const originOnly = new URL(newOrigin).origin
//See if origin is already in the list
if (!includes(allowedOrigins.value, originOnly)) {
//Add to the list
allowedOrigins.value.push(originOnly);
trigger();
//Save changes
await writeChanges()
//If current tab was added, reload the tab
if (!origin) {
await tabs.reload(currentTab.value?.id)
}
}
}
const removeOrigin = async (origin?: string): Promise => {
//Allow undefined to remove current origin
const delOrigin = defaultTo(origin, currentOrigin.value)
if (!delOrigin) {
return;
}
//Get origin part of url
const delOriginOnly = new URL(delOrigin).origin
const allowList = get(allowedOrigins)
//Remove the origin
allowedOrigins.value = filter(allowList, (o) => !isEqual(o, delOriginOnly));
trigger();
await writeChanges()
//If current tab was removed, reload the tab
if (!origin) {
await tabs.reload(currentTab.value?.id)
}
}
return {
addOrigin: popupAndOptionsOnly(addOrigin),
removeOrigin: popupAndOptionsOnly(removeOrigin),
enable: popupAndOptionsOnly(async (value: boolean): Promise => {
set(protectionEnabled, value)
await writeChanges()
}),
async getStatus(): Promise {
return{
allowedOrigins: [...get(allowedOrigins)],
enabled: get(protectionEnabled),
currentOrigin: get(currentOrigin),
isAllowed: isOriginAllowed()
}
},
waitForChange: () => {
//Wait for the trigger to change
return new Promise((resolve) => watchOnce([currentTab, protectionEnabled, manullyTriggered] as any, () => resolve()));
},
}
},
foreground: exportForegroundApi([
'addOrigin',
'removeOrigin',
'getStatus',
'enable',
'waitForChange'
])
}
}