From 539d2c60d9bead91d41d136863b2d83931f2d818 Mon Sep 17 00:00:00 2001 From: hensm Date: Sun, 29 May 2022 20:38:09 +0100 Subject: [PATCH] Re-work whitelist feature to allow per-site UA configuration --- CONTRIBUTING.md | 66 +++++- ext/src/_locales/de/messages.json | 26 +-- ext/src/_locales/en/messages.json | 38 ++-- ext/src/_locales/es/messages.json | 26 +-- ext/src/_locales/nl/messages.json | 22 +- ext/src/_locales/no/messages.json | 26 +-- ext/src/background/menus.ts | 8 +- ext/src/background/whitelist.ts | 31 ++- ext/src/defaultOptions.ts | 5 +- ext/src/lib/options.ts | 6 +- ext/src/ui/options/EditableList.tsx | 316 ---------------------------- ext/src/ui/options/Whitelist.tsx | 225 ++++++++++++++++++++ ext/src/ui/options/index.tsx | 72 ++----- ext/src/ui/options/styles/index.css | 38 ++-- ext/src/ui/popup/index.tsx | 31 +-- 15 files changed, 426 insertions(+), 510 deletions(-) delete mode 100644 ext/src/ui/options/EditableList.tsx create mode 100644 ext/src/ui/options/Whitelist.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a821746..52c2af7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,17 +35,43 @@ Missing/outdated strings: - `optionsMirroringCategoryDescription` - `optionsMirroringEnabled` - `optionsMirroringAppId` - - `popupWhitelistNotWhitelisted` - - `popupWhitelistAddToWhitelist` - - `popupMediaTypeAppNotFound` - `optionsBridgeCompatible` - `optionsBridgeLikelyCompatible` - `optionsBridgeIncompatible` + - `optionsSiteWhitelistCategoryName` + - `optionsSiteWhitelistCategoryDescription` + - `optionsSiteWhitelistEnabled` + - `optionsSiteWhitelistEnabledDescription` + - `optionsSiteWhitelistContent` + - `optionsSiteWhitelistBasicView` + - `optionsSiteWhitelistRawView` + - `optionsSiteWhitelistSaveRaw` + - `optionsSiteWhitelistAddItem` + - `optionsSiteWhitelistUserAgent` + - `optionsSiteWhitelistEditItem` + - `optionsSiteWhitelistRemoveItem` + - `optionsSiteWhitelistInvalidMatchPattern` + - `popupWhitelistNotWhitelisted` + - `popupWhitelistAddToWhitelist` + - `popupMediaTypeAppNotFound` - `popupCastMenuTitle` - `popupStopMenuTitle` - `es` + - `optionsSiteWhitelistCategoryName` + - `optionsSiteWhitelistCategoryDescription` + - `optionsSiteWhitelistEnabled` + - `optionsSiteWhitelistEnabledDescription` + - `optionsSiteWhitelistContent` + - `optionsSiteWhitelistBasicView` + - `optionsSiteWhitelistRawView` + - `optionsSiteWhitelistSaveRaw` + - `optionsSiteWhitelistAddItem` + - `optionsSiteWhitelistUserAgent` + - `optionsSiteWhitelistEditItem` + - `optionsSiteWhitelistRemoveItem` + - `optionsSiteWhitelistInvalidMatchPattern` - `popupWhitelistNotWhitelisted` - `popupWhitelistAddToWhitelist` - `popupCastMenuTitle` @@ -54,23 +80,47 @@ Missing/outdated strings: - `nl` - `optionsBridgeBackupEnabled` - - `optionsUserAgentWhitelistRestrictedEnabled` - - `optionsUserAgentWhitelistRestrictedEnabledDescription` + - `optionsBridgeCompatible` + - `optionsBridgeLikelyCompatible` + - `optionsBridgeIncompatible` - `optionsOptionRecommended` - `optionsMirroringCategoryName` - `optionsMirroringCategoryDescription` - `optionsMirroringEnabled` - `optionsMirroringAppId` + - `optionsSiteWhitelistCategoryName` + - `optionsSiteWhitelistCategoryDescription` + - `optionsSiteWhitelistEnabled` + - `optionsSiteWhitelistEnabledDescription` + - `optionsSiteWhitelistContent` + - `optionsSiteWhitelistBasicView` + - `optionsSiteWhitelistRawView` + - `optionsSiteWhitelistSaveRaw` + - `optionsSiteWhitelistAddItem` + - `optionsSiteWhitelistUserAgent` + - `optionsSiteWhitelistEditItem` + - `optionsSiteWhitelistRemoveItem` + - `optionsSiteWhitelistInvalidMatchPattern` - `popupWhitelistNotWhitelisted` - `popupWhitelistAddToWhitelist` - `popupMediaTypeAppNotFound` - - `optionsBridgeCompatible` - - `optionsBridgeLikelyCompatible` - - `optionsBridgeIncompatible` - `popupCastMenuTitle` - `popupStopMenuTitle` - `no` + - `optionsSiteWhitelistCategoryName` + - `optionsSiteWhitelistCategoryDescription` + - `optionsSiteWhitelistEnabled` + - `optionsSiteWhitelistEnabledDescription` + - `optionsSiteWhitelistContent` + - `optionsSiteWhitelistBasicView` + - `optionsSiteWhitelistRawView` + - `optionsSiteWhitelistSaveRaw` + - `optionsSiteWhitelistAddItem` + - `optionsSiteWhitelistUserAgent` + - `optionsSiteWhitelistEditItem` + - `optionsSiteWhitelistRemoveItem` + - `optionsSiteWhitelistInvalidMatchPattern` - `popupWhitelistNotWhitelisted` - `popupWhitelistAddToWhitelist` - `popupCastMenuTitle` diff --git a/ext/src/_locales/de/messages.json b/ext/src/_locales/de/messages.json index 95b332a..9cd1f3f 100644 --- a/ext/src/_locales/de/messages.json +++ b/ext/src/_locales/de/messages.json @@ -259,55 +259,55 @@ "description": "Receiver selector close if focus lost option checkbox label." }, - "optionsUserAgentWhitelistCategoryName": { + "optionsSiteWhitelistCategoryName": { "message": "Useragent-Whitelist", "description": "Options page whitelist category title." }, - "optionsUserAgentWhitelistCategoryDescription": { + "optionsSiteWhitelistCategoryDescription": { "message": "Seiten auf denen der Useragent aus Kompatibilitätsgründen mit einer Chrome-Version ersetzt wird. Suchmuster müssen gültig sein.", "description": "Options page whitelist category description." }, - "optionsUserAgentWhitelistEnabled": { + "optionsSiteWhitelistEnabled": { "message": "Webseiten-Whitelist aktivieren", "description": "Whitelist enabled checkbox label." }, - "optionsUserAgentWhitelistRestrictedEnabled": { + "optionsSiteWhitelistRestrictedEnabled": { "message": "Eingeschränkten Modus aktivieren", "description": "Whitelist restricted mode enabled checkbox label." }, - "optionsUserAgentWhitelistRestrictedEnabledDescription": { + "optionsSiteWhitelistRestrictedEnabledDescription": { "message": "Whitelist-Einschränkungen auch auf Seiten anwenden, die unabhängig vom aktuellen Useragent versuchen Stream-Funktionen zu laden.", "description": "Whitelist restricted mode enabled description." }, - "optionsUserAgentWhitelistContent": { + "optionsSiteWhitelistContent": { "message": "Suchmuster:", "description": "Match patterns editor widget label." }, - "optionsUserAgentWhitelistBasicView": { + "optionsSiteWhitelistBasicView": { "message": "Einfache Ansicht", "description": "Switch to basic view button title." }, - "optionsUserAgentWhitelistRawView": { + "optionsSiteWhitelistRawView": { "message": "Rohdatenansicht", "description": "Switch to raw view button title." }, - "optionsUserAgentWhitelistSaveRaw": { + "optionsSiteWhitelistSaveRaw": { "message": "Rohdaten speichern", "description": "Save raw view edits button title." }, - "optionsUserAgentWhitelistAddItem": { + "optionsSiteWhitelistAddItem": { "message": "Eintrag hinzufügen", "description": "Add new whitelist item button title." }, - "optionsUserAgentWhitelistEditItem": { + "optionsSiteWhitelistEditItem": { "message": "Bearbeiten", "description": "Edit whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistRemoveItem": { + "optionsSiteWhitelistRemoveItem": { "message": "Entfernen", "description": "Remove whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistInvalidMatchPattern": { + "optionsSiteWhitelistInvalidMatchPattern": { "message": "Ungültiges Suchmuster $matchPattern$", "description": "Error displayed by input indicating an invalid match pattern.", "placeholders": { diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index c61c4b4..9524953 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -302,55 +302,55 @@ "description": "Receiver selector close if focus lost option checkbox label." }, - "optionsUserAgentWhitelistCategoryName": { - "message": "User agent whitelist", + "optionsSiteWhitelistCategoryName": { + "message": "Site whitelist", "description": "Options page whitelist category title." }, - "optionsUserAgentWhitelistCategoryDescription": { - "message": "Sites for which to replace the user agent with a Chrome version for compatibility. Must be valid match patterns.", + "optionsSiteWhitelistCategoryDescription": { + "message": "Site where cast functionality will be enabled and the user agent string will be replaced with a Chrome version for compatibility.", "description": "Options page whitelist category description." }, - "optionsUserAgentWhitelistEnabled": { + "optionsSiteWhitelistEnabled": { "message": "Enable site whitelist", "description": "Whitelist enabled checkbox label." }, - "optionsUserAgentWhitelistRestrictedEnabled": { - "message": "Enable restricted mode", - "description": "Whitelist restricted mode enabled checkbox label." - }, - "optionsUserAgentWhitelistRestrictedEnabledDescription": { - "message": "Also apply whitelist restrictions to sites attempting to load cast functionality regardless of the current user agent.", + "optionsSiteWhitelistEnabledDescription": { + "message": "Disabling this option will enable cast functionality on any site, but the user agent string will not be replaced.", "description": "Whitelist restricted mode enabled description." }, - "optionsUserAgentWhitelistContent": { + "optionsSiteWhitelistContent": { "message": "Match patterns:", "description": "Match patterns editor widget label." }, - "optionsUserAgentWhitelistBasicView": { + "optionsSiteWhitelistBasicView": { "message": "Basic View", "description": "Switch to basic view button title." }, - "optionsUserAgentWhitelistRawView": { + "optionsSiteWhitelistRawView": { "message": "Raw View", "description": "Switch to raw view button title." }, - "optionsUserAgentWhitelistSaveRaw": { + "optionsSiteWhitelistSaveRaw": { "message": "Save Raw", "description": "Save raw view edits button title." }, - "optionsUserAgentWhitelistAddItem": { + "optionsSiteWhitelistAddItem": { "message": "Add Item", "description": "Add new whitelist item button title." }, - "optionsUserAgentWhitelistEditItem": { + "optionsSiteWhitelistUserAgent": { + "message": "Enable UA", + "description": "Whitelist item user agent checkbox title." + }, + "optionsSiteWhitelistEditItem": { "message": "Edit", "description": "Edit whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistRemoveItem": { + "optionsSiteWhitelistRemoveItem": { "message": "Remove", "description": "Remove whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistInvalidMatchPattern": { + "optionsSiteWhitelistInvalidMatchPattern": { "message": "Invalid match pattern $matchPattern$", "description": "Error displayed by input indicating an invalid match pattern.", "placeholders": { diff --git a/ext/src/_locales/es/messages.json b/ext/src/_locales/es/messages.json index 1397c35..55afeec 100644 --- a/ext/src/_locales/es/messages.json +++ b/ext/src/_locales/es/messages.json @@ -275,55 +275,55 @@ "description": "Receiver selector close if focus lost option checkbox label." }, - "optionsUserAgentWhitelistCategoryName": { + "optionsSiteWhitelistCategoryName": { "message": "Lista blanca de agentes de usuario", "description": "Options page whitelist category title." }, - "optionsUserAgentWhitelistCategoryDescription": { + "optionsSiteWhitelistCategoryDescription": { "message": "Sitios en los cuales reemplazar el agente de usuario con una versión de Chrome para compatibilidad. Deben ser patrones de coincidencia válidos.", "description": "Options page whitelist category description." }, - "optionsUserAgentWhitelistEnabled": { + "optionsSiteWhitelistEnabled": { "message": "Activar lista blanca de sitios", "description": "Whitelist enabled checkbox label." }, - "optionsUserAgentWhitelistRestrictedEnabled": { + "optionsSiteWhitelistRestrictedEnabled": { "message": "Activar modo restringido", "description": "Whitelist restricted mode enabled checkbox label." }, - "optionsUserAgentWhitelistRestrictedEnabledDescription": { + "optionsSiteWhitelistRestrictedEnabledDescription": { "message": "También aplica restricciones de la lista blanca a sitios intentando cargar la funcionalidad de transmisión sin importar el agente de usuario actual.", "description": "Whitelist restricted mode enabled description." }, - "optionsUserAgentWhitelistContent": { + "optionsSiteWhitelistContent": { "message": "Patrones de coincidencia:", "description": "Match patterns editor widget label." }, - "optionsUserAgentWhitelistBasicView": { + "optionsSiteWhitelistBasicView": { "message": "Vista básica", "description": "Switch to basic view button title." }, - "optionsUserAgentWhitelistRawView": { + "optionsSiteWhitelistRawView": { "message": "Vista en bruto", "description": "Switch to raw view button title." }, - "optionsUserAgentWhitelistSaveRaw": { + "optionsSiteWhitelistSaveRaw": { "message": "Guardar archivo en bruto", "description": "Save raw view edits button title." }, - "optionsUserAgentWhitelistAddItem": { + "optionsSiteWhitelistAddItem": { "message": "Añadir elemento", "description": "Add new whitelist item button title." }, - "optionsUserAgentWhitelistEditItem": { + "optionsSiteWhitelistEditItem": { "message": "Editar", "description": "Edit whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistRemoveItem": { + "optionsSiteWhitelistRemoveItem": { "message": "Eliminar", "description": "Remove whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistInvalidMatchPattern": { + "optionsSiteWhitelistInvalidMatchPattern": { "message": "Patrón de coincidencia $matchPattern$ inválido", "description": "Error displayed by input indicating an invalid match pattern.", "placeholders": { diff --git a/ext/src/_locales/nl/messages.json b/ext/src/_locales/nl/messages.json index 4f8e4cd..d7e2f9d 100755 --- a/ext/src/_locales/nl/messages.json +++ b/ext/src/_locales/nl/messages.json @@ -243,47 +243,47 @@ "message": "Sluit na het verliezen van de focus", "description": "Receiver selector close if focus lost option checkbox label." }, - "optionsUserAgentWhitelistCategoryName": { + "optionsSiteWhitelistCategoryName": { "message": "Gebruikersagent - Whitelist", "description": "Options page whitelist category title." }, - "optionsUserAgentWhitelistCategoryDescription": { + "optionsSiteWhitelistCategoryDescription": { "message": "Websites waarvan de gebruikersagent omwille van compatibiliteit moet worden ingesteld op Chrome. De patronen moeten geldig zijn.", "description": "Options page whitelist category description." }, - "optionsUserAgentWhitelistEnabled": { + "optionsSiteWhitelistEnabled": { "message": "Whitelist ingeschakeld", "description": "Whitelist enabled checkbox label." }, - "optionsUserAgentWhitelistContent": { + "optionsSiteWhitelistContent": { "message": "Patronen:", "description": "Match patterns editor widget label." }, - "optionsUserAgentWhitelistBasicView": { + "optionsSiteWhitelistBasicView": { "message": "Basisweergave", "description": "Switch to basic view button title." }, - "optionsUserAgentWhitelistRawView": { + "optionsSiteWhitelistRawView": { "message": "Ruwe weergave", "description": "Switch to raw view button title." }, - "optionsUserAgentWhitelistSaveRaw": { + "optionsSiteWhitelistSaveRaw": { "message": "Ruwe weergave opslaan", "description": "Save raw view edits button title." }, - "optionsUserAgentWhitelistAddItem": { + "optionsSiteWhitelistAddItem": { "message": "Voeg toe", "description": "Add new whitelist item button title." }, - "optionsUserAgentWhitelistEditItem": { + "optionsSiteWhitelistEditItem": { "message": "Bewerken", "description": "Edit whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistRemoveItem": { + "optionsSiteWhitelistRemoveItem": { "message": "Verwijderen", "description": "Remove whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistInvalidMatchPattern": { + "optionsSiteWhitelistInvalidMatchPattern": { "message": "Ongeldig patroon $matchPattern$", "description": "Error displayed by input indicating an invalid match pattern.", "placeholders": { diff --git a/ext/src/_locales/no/messages.json b/ext/src/_locales/no/messages.json index 32b8467..f31c525 100644 --- a/ext/src/_locales/no/messages.json +++ b/ext/src/_locales/no/messages.json @@ -263,55 +263,55 @@ "description": "Receiver selector close if focus lost option checkbox label." }, - "optionsUserAgentWhitelistCategoryName": { + "optionsSiteWhitelistCategoryName": { "message": "Brukeragent whitelist", "description": "Options page whitelist category title." }, - "optionsUserAgentWhitelistCategoryDescription": { + "optionsSiteWhitelistCategoryDescription": { "message": "Sider hvor man kan erstatte brukeragent med en Chrome-versjon for kompatibilitet. Må være et gjenkjennbart mønster.", "description": "Options page whitelist category description." }, - "optionsUserAgentWhitelistEnabled": { + "optionsSiteWhitelistEnabled": { "message": "Skru på whitelist", "description": "Whitelist enabled checkbox label." }, - "optionsUserAgentWhitelistRestrictedEnabled": { + "optionsSiteWhitelistRestrictedEnabled": { "message": "Skru på ", "description": "Whitelist restricted mode enabled checkbox label." }, - "optionsUserAgentWhitelistRestrictedEnabledDescription": { + "optionsSiteWhitelistRestrictedEnabledDescription": { "message": "Legg også til whitelist-begrensninger til side smo prøvde å laste cast-funksjonalitet uavhengig av nåværende brukeragent.", "description": "Whitelist restricted mode enabled description." }, - "optionsUserAgentWhitelistContent": { + "optionsSiteWhitelistContent": { "message": "Match mønster", "description": "Match patterns editor widget label." }, - "optionsUserAgentWhitelistBasicView": { + "optionsSiteWhitelistBasicView": { "message": "Standard visning", "description": "Switch to basic view button title." }, - "optionsUserAgentWhitelistRawView": { + "optionsSiteWhitelistRawView": { "message": "Rå visning", "description": "Switch to raw view button title." }, - "optionsUserAgentWhitelistSaveRaw": { + "optionsSiteWhitelistSaveRaw": { "message": "Lagre rå", "description": "Save raw view edits button title." }, - "optionsUserAgentWhitelistAddItem": { + "optionsSiteWhitelistAddItem": { "message": "Legg til", "description": "Add new whitelist item button title." }, - "optionsUserAgentWhitelistEditItem": { + "optionsSiteWhitelistEditItem": { "message": "Rediger", "description": "Edit whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistRemoveItem": { + "optionsSiteWhitelistRemoveItem": { "message": "Fjern", "description": "Remove whitelist item button title. Displayed on each item." }, - "optionsUserAgentWhitelistInvalidMatchPattern": { + "optionsSiteWhitelistInvalidMatchPattern": { "message": "Ugyldig mønster $matchPattern$", "description": "Error displayed by input indicating an invalid match pattern.", "placeholders": { diff --git a/ext/src/background/menus.ts b/ext/src/background/menus.ts index fff31b2..9c0fc1d 100644 --- a/ext/src/background/menus.ts +++ b/ext/src/background/menus.ts @@ -137,11 +137,11 @@ async function onMenuClicked( ); } - const whitelist = await options.get("userAgentWhitelist"); - if (!whitelist.includes(pattern)) { + const whitelist = await options.get("siteWhitelist"); + if (!whitelist.find(item => item.pattern === pattern)) { // Add to whitelist and update options - whitelist.push(pattern); - await options.set("userAgentWhitelist", whitelist); + whitelist.push({ pattern }); + await options.set("siteWhitelist", whitelist); } return; diff --git a/ext/src/background/whitelist.ts b/ext/src/background/whitelist.ts index 2e03d78..f05030a 100644 --- a/ext/src/background/whitelist.ts +++ b/ext/src/background/whitelist.ts @@ -22,6 +22,11 @@ type OnBeforeRequestDetails = Parameters< frameAncestors?: Array<{ url: string; frameId: number }>; }; +export interface WhitelistItemData { + pattern: string; + isUserAgentDisabled?: boolean; +} + const originUrlCache: string[] = []; let platform: string; @@ -47,18 +52,18 @@ export async function initWhitelist() { } // Register on first run - await registerUserAgentWhitelist(); + await registerSiteWhitelist(); // Re-register when options change options.addEventListener("changed", ev => { const alteredOpts = ev.detail; if ( - alteredOpts.includes("userAgentWhitelist") || - alteredOpts.includes("userAgentWhitelistEnabled") + alteredOpts.includes("siteWhitelist") || + alteredOpts.includes("siteWhitelistEnabled") ) { - unregisterUserAgentWhitelist(); - registerUserAgentWhitelist(); + unregisterSiteWhitelist(); + registerSiteWhitelist(); } }); } @@ -189,9 +194,8 @@ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) { }; } -async function registerUserAgentWhitelist() { - const { userAgentWhitelist, userAgentWhitelistEnabled } = - await options.getAll(); +async function registerSiteWhitelist() { + const { siteWhitelist, siteWhitelistEnabled } = await options.getAll(); browser.webRequest.onBeforeRequest.addListener( onBeforeCastSDKRequest, @@ -199,13 +203,18 @@ async function registerUserAgentWhitelist() { ["blocking"] ); - if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) { + if (!siteWhitelistEnabled || !siteWhitelist.length) { return; } browser.webRequest.onBeforeSendHeaders.addListener( onWhitelistedBeforeSendHeaders, - { urls: userAgentWhitelist }, + { + // Filter for items with UA enabled + urls: siteWhitelist.flatMap(item => + !item.isUserAgentDisabled ? [item.pattern] : [] + ) + }, ["blocking", "requestHeaders"] ); @@ -216,7 +225,7 @@ async function registerUserAgentWhitelist() { ); } -function unregisterUserAgentWhitelist() { +function unregisterSiteWhitelist() { originUrlCache.length = 0; browser.webRequest.onBeforeSendHeaders.removeListener( diff --git a/ext/src/defaultOptions.ts b/ext/src/defaultOptions.ts index 90913cc..cd03d25 100644 --- a/ext/src/defaultOptions.ts +++ b/ext/src/defaultOptions.ts @@ -16,7 +16,6 @@ export default { mirroringAppId: MIRRORING_APP_ID, receiverSelectorCloseIfFocusLost: true, receiverSelectorWaitForConnection: true, - userAgentWhitelistEnabled: true, - userAgentWhitelistRestrictedEnabled: true, - userAgentWhitelist: ["https://www.netflix.com/*"] + siteWhitelistEnabled: true, + siteWhitelist: [{ pattern: "https://www.netflix.com/*" }] } as Options; diff --git a/ext/src/lib/options.ts b/ext/src/lib/options.ts index 03dc7fb..fdf0955 100644 --- a/ext/src/lib/options.ts +++ b/ext/src/lib/options.ts @@ -1,6 +1,7 @@ "use strict"; import defaultOptions from "../defaultOptions"; +import type { WhitelistItemData } from "../background/whitelist"; import logger from "./logger"; @@ -25,9 +26,8 @@ export interface Options { mirroringAppId: string; receiverSelectorCloseIfFocusLost: boolean; receiverSelectorWaitForConnection: boolean; - userAgentWhitelistEnabled: boolean; - userAgentWhitelistRestrictedEnabled: boolean; - userAgentWhitelist: string[]; + siteWhitelistEnabled: boolean; + siteWhitelist: WhitelistItemData[]; [key: string]: Options[keyof Options]; } diff --git a/ext/src/ui/options/EditableList.tsx b/ext/src/ui/options/EditableList.tsx deleted file mode 100644 index d50f5c1..0000000 --- a/ext/src/ui/options/EditableList.tsx +++ /dev/null @@ -1,316 +0,0 @@ -/* eslint-disable max-len */ -"use strict"; - -import React, { Component } from "react"; - -const _ = browser.i18n.getMessage; - - -interface EditableListProps { - data: string[]; - itemPattern: RegExp; - onChange (data: string[]): void; - itemPatternError (err?: string): string; -} - -interface EditableListState { - addingNewItem: boolean; - rawView: boolean; - rawViewValue: string; -} - -export default class EditableList extends Component< - EditableListProps, EditableListState> { - - private rawViewTextArea: (HTMLTextAreaElement | null) = null; - - constructor(props: EditableListProps) { - super(props); - - this.state = { - addingNewItem: false - , rawView: false - , rawViewValue: "" - }; - - this.handleItemRemove = this.handleItemRemove.bind(this); - this.handleItemEdit = this.handleItemEdit.bind(this); - this.handleSwitchView = this.handleSwitchView.bind(this); - this.handleSaveRaw = this.handleSaveRaw.bind(this); - this.handleRawViewTextAreaChange = this.handleRawViewTextAreaChange.bind(this); - this.handleAddItem = this.handleAddItem.bind(this); - this.handleNewItemRemove = this.handleNewItemRemove.bind(this); - this.handleNewItemEdit = this.handleNewItemEdit.bind(this); - } - - public render() { - return ( -
- { this.state.rawView - ? ( - - ) : ( - - )} -
-
- { !this.state.rawView && - } - - { this.state.rawView && - } - - -
-
- ); - } - - private handleItemRemove(item: string) { - const newItems = new Set(this.props.data); - newItems.delete(item); - - this.props.onChange([ ...newItems ]); - } - - private handleItemEdit(item: string, newValue: string) { - this.props.onChange(this.props.data.map( - currentItem => currentItem === item - ? newValue - : currentItem)); - } - - private handleSwitchView() { - this.setState(currentState => { - if (currentState.rawView) { - return { - rawView: false - , rawViewValue: "" - }; - } - - return { - rawView: true - , rawViewValue: this.props.data.join("\n") - }; - }); - } - - private handleSaveRaw() { - this.setState(currentState => { - const newItems = currentState.rawViewValue.split("\n") - .filter(item => item !== ""); - - if ("itemPattern" in this.props) { - for (const item of newItems) { - if (!this.props.itemPattern.test(item)) { - this.rawViewTextArea?.setCustomValidity( - this.props.itemPatternError(item)); - return; - } - } - - this.rawViewTextArea?.setCustomValidity(""); - } - - this.props.onChange(newItems); - }); - } - - private handleRawViewTextAreaChange(ev: React.ChangeEvent) { - if (!this.rawViewTextArea) { - return; - } - - if (this.rawViewTextArea.scrollHeight > this.rawViewTextArea.clientHeight) { - this.rawViewTextArea.style.height = `${this.rawViewTextArea.scrollHeight}px`; - } - - this.setState({ - rawViewValue: ev.target.value - }); - } - - private handleAddItem() { - this.setState({ - addingNewItem: true - }); - } - - private handleNewItemRemove() { - this.setState({ - addingNewItem: false - }); - } - - private handleNewItemEdit(_item: string, newItem: string) { - this.setState({ - addingNewItem: false - }, () => { - this.props.onChange([ ...this.props.data, newItem ]); - }); - } -} - - -interface EditableListItemProps { - text: string; - itemPattern: RegExp; - editing?: boolean; - itemPatternError (err?: string): string; - onRemove (item: string): void; - onEdit (item: string, newValue: string): void; -} - -interface EditableListItemState { - editing: boolean; - editValue: string; -} - -class EditableListItem extends Component< - EditableListItemProps, EditableListItemState> { - - private input: (HTMLInputElement | null) = null; - - constructor(props: EditableListItemProps) { - super(props); - - this.state = { - editing: this.props.editing || false - , editValue: "" - }; - - this.handleRemove = this.handleRemove.bind(this); - this.handleEditBegin = this.handleEditBegin.bind(this); - this.handleEditEnd = this.handleEditEnd.bind(this); - this.handleInputChange = this.handleInputChange.bind(this); - this.handleInputKeyPress = this.handleInputKeyPress.bind(this); - } - - public render() { - const selected = this.state.editing - ? "editable-list__item--selected" : ""; - - return ( -
  • -
    - - { this.state.editing - ? this.input = input } - value={ this.state.editValue } - onBlur={ this.handleEditEnd } - onChange={ this.handleInputChange } - onKeyPress={ this.handleInputKeyPress }/> - : this.props.text } -
    - - - - -
  • - ); - } - - private stopEditing(input: HTMLInputElement) { - if (this.props.editing - && !this.props.itemPattern.test(this.state.editValue)) { - input.setCustomValidity(this.props.itemPatternError()); - } - - if (!input.validity.valid) { - return; - } - - this.props.onEdit(this.props.text, this.state.editValue); - this.setState({ - editing: false - , editValue: "" - }); - } - - private handleRemove() { - this.props.onRemove(this.props.text); - } - - private handleEditBegin() { - if (!this.state.editing) { - this.setState({ - editing: true - , editValue: this.props.text - }, () => { - this.input?.focus(); - this.input?.select(); - }); - } - } - - private handleEditEnd(ev: React.FocusEvent) { - this.stopEditing(ev.target); - } - - private handleInputChange(ev: React.ChangeEvent) { - this.setState({ - editValue: ev.target.value - }); - - // If invalid, set custom error from parent - ev.target.setCustomValidity(!this.props.itemPattern.test(ev.target.value) - ? this.props.itemPatternError() - : ""); - } - - private handleInputKeyPress(ev: React.KeyboardEvent) { - if (ev.key === "Enter") { - this.stopEditing(ev.target as HTMLInputElement); - } - } -} diff --git a/ext/src/ui/options/Whitelist.tsx b/ext/src/ui/options/Whitelist.tsx new file mode 100644 index 0000000..a5eff22 --- /dev/null +++ b/ext/src/ui/options/Whitelist.tsx @@ -0,0 +1,225 @@ +/* eslint-disable max-len */ +"use strict"; + +import React, { Component } from "react"; + +import type { WhitelistItemData } from "../../background/whitelist"; +import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/matchPattern"; + +const _ = browser.i18n.getMessage; + +interface WhitelistProps { + items: WhitelistItemData[]; + onChange: (items: WhitelistItemData[]) => void; +} +interface WhitelistState { + addingNewItem: boolean; +} + +/** Editable list component for site whitelist. */ +export default class Whitelist extends Component< + WhitelistProps, + WhitelistState +> { + state: WhitelistState = { + addingNewItem: false + }; + + render() { + return ( +
    +
      + {this.props.items.map((item, i) => ( + { + // Replace item + this.props.onChange( + this.props.items.map(item => + item.pattern === oldValue?.pattern + ? newValue + : item + ) + ); + }} + onRemove={value => { + // Remove item + this.props.onChange( + this.props.items.filter( + item => item.pattern !== value?.pattern + ) + ); + }} + key={i} + /> + ))} + + {this.state.addingNewItem && ( + { + // Add new item + this.setState({ addingNewItem: false }, () => { + this.props.onChange([ + ...this.props.items, + newValue + ]); + }); + }} + onRemove={() => { + // Cancel adding new item + this.setState({ addingNewItem: false }); + }} + /> + )} +
    + +
    + +
    + +
    +
    + ); + } +} + +interface WhitelistItemProps { + value?: WhitelistItemData; + /** Initial editing state */ + isEditing?: boolean; + onEdit: ( + oldValue: WhitelistItemData | undefined, + newValue: WhitelistItemData + ) => void; + onRemove: (value?: WhitelistItemData) => void; +} +interface WhitelistItemState { + isEditing: boolean; + editValue?: WhitelistItemData; +} + +/** Editable item component for whitelist. */ +class WhitelistItem extends Component { + private inputElement: HTMLInputElement | null = null; + + constructor(props: WhitelistItemProps) { + super(props); + this.state = { isEditing: props.isEditing ?? false }; + + this.beginEditing = this.beginEditing.bind(this); + this.finishEditing = this.finishEditing.bind(this); + } + + /** Sets editing state and focuses input field. */ + private beginEditing() { + if (this.state.isEditing) return; + + this.setState( + { + isEditing: true, + editValue: this.props.value + }, + () => { + this.inputElement?.focus(); + this.inputElement?.select(); + } + ); + } + + /** Checks input validity and sends edit update. */ + private finishEditing() { + if (!this.state.isEditing || !this.state.editValue) return; + + if (this.inputElement?.validity.valid) { + this.props.onEdit(this.props.value, this.state.editValue); + this.setState({ + isEditing: false, + editValue: undefined + }); + } + } + + render() { + const selectedClassName = this.state.isEditing + ? "whitelist__item--selected" + : ""; + + return ( +
  • +
    + {this.state.isEditing ? ( + (this.inputElement = el)} + type="text" + className="whitelist__input-pattern" + value={this.state.editValue?.pattern} + pattern={REMOTE_MATCH_PATTERN_REGEX.source} + onChange={ev => { + this.setState(prevState => ({ + editValue: { + ...prevState.editValue, + pattern: ev.target.value + } + })); + }} + onBlur={this.finishEditing} + onKeyPress={ev => { + if (ev.key === "Enter") { + this.finishEditing(); + } + }} + /> + ) : ( + this.props.value?.pattern + )} +
    + + + + + + +
  • + ); + } +} diff --git a/ext/src/ui/options/index.tsx b/ext/src/ui/options/index.tsx index 07a438f..8c53313 100644 --- a/ext/src/ui/options/index.tsx +++ b/ext/src/ui/options/index.tsx @@ -5,14 +5,14 @@ import React, { Component } from "react"; import ReactDOM from "react-dom"; import defaultOptions from "../../defaultOptions"; +import type { WhitelistItemData } from "../../background/whitelist"; import Bridge from "./Bridge"; -import EditableList from "./EditableList"; +import Whitelist from "./Whitelist"; import bridge, { BridgeInfo, BridgeTimedOutError } from "../../lib/bridge"; import logger from "../../lib/logger"; import options, { Options } from "../../lib/options"; -import { REMOTE_MATCH_PATTERN_REGEX } from "../../lib/matchPattern"; const _ = browser.i18n.getMessage; @@ -77,9 +77,6 @@ class OptionsApp extends Component { this.handleFormChange = this.handleFormChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this); this.handleWhitelistChange = this.handleWhitelistChange.bind(this); - - this.getWhitelistItemPatternError = - this.getWhitelistItemPatternError.bind(this); } public async componentDidMount() { @@ -333,77 +330,43 @@ class OptionsApp extends Component {
    -

    - {_("optionsUserAgentWhitelistCategoryName")} -

    +

    {_("optionsSiteWhitelistCategoryName")}

    - {_("optionsUserAgentWhitelistCategoryDescription")} + {_("optionsSiteWhitelistCategoryDescription")}

    - -
    - {_("optionsUserAgentWhitelistContent")} + {_("optionsSiteWhitelistContent")}
    {this.state.options?.userAgentWhitelist && ( - )}
    @@ -485,22 +448,15 @@ class OptionsApp extends Component { }); } - private handleWhitelistChange(whitelist: string[]) { + private handleWhitelistChange(whitelist: WhitelistItemData[]) { this.setState(currentState => { if (currentState.options) { - currentState.options.userAgentWhitelist = whitelist; + currentState.options.siteWhitelist = whitelist; } return currentState; }); } - - private getWhitelistItemPatternError(info: string): string { - return _("optionsUserAgentWhitelistInvalidMatchPattern", info); - } } - -ReactDOM.render( - - , document.querySelector("#root")); +ReactDOM.render(, document.querySelector("#root")); diff --git a/ext/src/ui/options/styles/index.css b/ext/src/ui/options/styles/index.css index c5bf68f..4b5bb10 100644 --- a/ext/src/ui/options/styles/index.css +++ b/ext/src/ui/options/styles/index.css @@ -316,7 +316,7 @@ button.ghost:not(:hover) { } -.editable-list { +.whitelist { background-color: var(--box-background); border: 1px solid var(--border-color); color: var(--box-color); @@ -324,22 +324,18 @@ button.ghost:not(:hover) { padding: 5px; } -.editable-list__view-actions { +.whitelist__view-actions { display: flex; justify-content: end; } -.editable-list__save-raw-button { - margin-inline-end: 5px; -} - -.editable-list hr { +.whitelist hr { border: initial; border-top: 1px solid var(--border-color); margin: 5px 0; } -.editable-list__items { +.whitelist__items { display: flex; flex-direction: column; margin: initial; @@ -348,22 +344,22 @@ button.ghost:not(:hover) { width: calc(100% + 10px); } -.editable-list__item { +.whitelist__item { align-items: center; display: flex; height: 34px; padding: 0 5px; } -.editable-list__item:nth-child(odd) { +.whitelist__item:nth-child(odd) { background-color: rgba(0, 0, 0, 0.05); } -.editable-list__item--selected { +.whitelist__item--selected { background-color: var(--blue-50-a30) !important; } -.editable-list__title { +.whitelist__title { flex: 1; font-family: monospace; overflow: hidden; @@ -372,28 +368,24 @@ button.ghost:not(:hover) { white-space: nowrap; } -.editable-list__item:not(.editable-list__item--selected) > .editable-list__title { +.whitelist__item:not(.whitelist__item--selected) > .whitelist__title { padding: 0 8px; } -.editable-list__title + button { +.whitelist__title + button { margin-inline-end: 5px; } -.editable-list__edit-field { +.whitelist__input-pattern { font: inherit; margin-inline-end: 1em; width: -moz-available; } -.editable-list__raw-view { - max-height: 300px; - overflow-y: auto; - resize: vertical; - width: 100%; +.whitelist__user-agent { + margin-inline-end: 5px; } - -.editable-list__add-button { +.whitelist__add-button { margin-inline-end: auto; } @@ -410,7 +402,7 @@ button.ghost:not(:hover) { @media (prefers-color-scheme: dark) { - .editable-list__item:nth-child(odd) { + .whitelist__item:nth-child(odd) { background-color: rgba(255, 255, 255, 0.05); } } diff --git a/ext/src/ui/popup/index.tsx b/ext/src/ui/popup/index.tsx index 1dab37d..8e03251 100755 --- a/ext/src/ui/popup/index.tsx +++ b/ext/src/ui/popup/index.tsx @@ -5,6 +5,8 @@ import React, { Component } from "react"; import ReactDOM from "react-dom"; import { menuIdPopupCast, menuIdPopupStop } from "../../background/menus"; +import type { ReceiverSelectorPageInfo } from "../../background/ReceiverSelector"; +import type { WhitelistItemData } from "../../background/whitelist"; import knownApps, { KnownApp } from "../../cast/knownApps"; import options from "../../lib/options"; @@ -20,7 +22,6 @@ import { ReceiverSelectorMediaType } from "../../types"; -import { ReceiverSelectorPageInfo } from "../../background/ReceiverSelector"; import { Capability } from "../../cast/sdk/enums"; const _ = browser.i18n.getMessage; @@ -92,8 +93,8 @@ interface PopupAppState { // Options mirroringEnabled: boolean; - userAgentWhitelistEnabled: boolean; - userAgentWhitelist: string[]; + siteWhitelistEnabled: boolean; + siteWhitelist: WhitelistItemData[]; } class PopupApp extends Component { @@ -114,8 +115,8 @@ class PopupApp extends Component { isPageWhitelisted: false, isConnecting: false, mirroringEnabled: false, - userAgentWhitelistEnabled: true, - userAgentWhitelist: [] + siteWhitelistEnabled: true, + siteWhitelist: [] }; // Store window ref @@ -229,8 +230,8 @@ class PopupApp extends Component { // Check if target page URL is whitelisted. if (this.state.pageInfo) { - for (const patternString of this.state.userAgentWhitelist) { - const pattern = new RemoteMatchPattern(patternString); + for (const item of this.state.siteWhitelist) { + const pattern = new RemoteMatchPattern(item.pattern); if (pattern.matches(this.state.pageInfo.url)) { isPageWhitelisted = true; break; @@ -249,10 +250,10 @@ class PopupApp extends Component { return; } - const whitelist = await options.get("userAgentWhitelist"); - if (!whitelist.includes(app.matches)) { - whitelist.push(app.matches); - await options.set("userAgentWhitelist", whitelist); + const whitelist = await options.get("siteWhitelist"); + if (!whitelist.find(item => item.pattern === app.matches)) { + whitelist.push({ pattern: app.matches }); + await options.set("siteWhitelist", whitelist); await browser.tabs.reload(pageInfo.tabId); window.close(); @@ -382,8 +383,8 @@ class PopupApp extends Component { options.getAll().then(opts => { this.setState({ mirroringEnabled: opts.mirroringEnabled, - userAgentWhitelistEnabled: opts.userAgentWhitelistEnabled, - userAgentWhitelist: opts.userAgentWhitelist + siteWhitelistEnabled: opts.siteWhitelistEnabled, + siteWhitelist: opts.siteWhitelist }); }); @@ -429,9 +430,9 @@ class PopupApp extends Component { // If we don't know the app !this.state.knownApp || // If the whitelist is disabled - !this.state.userAgentWhitelistEnabled || + !this.state.siteWhitelistEnabled || // If the whitelist is enabled, and the page is whitelisted - (this.state.userAgentWhitelistEnabled && + (this.state.siteWhitelistEnabled && this.state.isPageWhitelisted) || // If an app is already loaded on the page !!(