mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-11 10:09:59 +00:00
Rename directory: ext -> extension
This commit is contained in:
297
extension/src/background/ReceiverSelector.ts
Normal file
297
extension/src/background/ReceiverSelector.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import logger from "../lib/logger";
|
||||
import messaging, { Port, Message } from "../messaging";
|
||||
import options from "../lib/options";
|
||||
import { TypedEventTarget } from "../lib/TypedEventTarget";
|
||||
|
||||
import type { SenderMediaMessage, SenderMessage } from "../cast/sdk/types";
|
||||
import type {
|
||||
ReceiverDevice,
|
||||
ReceiverSelectorAppInfo,
|
||||
ReceiverSelectorMediaType,
|
||||
ReceiverSelectorPageInfo
|
||||
} from "../types";
|
||||
|
||||
const POPUP_URL = browser.runtime.getURL("ui/popup/index.html");
|
||||
|
||||
export interface ReceiverSelection {
|
||||
device: ReceiverDevice;
|
||||
mediaType: ReceiverSelectorMediaType;
|
||||
}
|
||||
|
||||
export interface ReceiverSelectorReceiverMessage {
|
||||
deviceId: string;
|
||||
message: SenderMessage;
|
||||
}
|
||||
export interface ReceiverSelectorMediaMessage {
|
||||
deviceId: string;
|
||||
message: SenderMediaMessage;
|
||||
}
|
||||
|
||||
interface ReceiverSelectorEvents {
|
||||
selected: ReceiverSelection;
|
||||
cancelled: void;
|
||||
stop: { deviceId: string };
|
||||
error: string;
|
||||
close: void;
|
||||
receiverMessage: ReceiverSelectorReceiverMessage;
|
||||
mediaMessage: ReceiverSelectorMediaMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the receiver selector popup window and communication with the
|
||||
* extension page hosted within.
|
||||
*/
|
||||
export default class ReceiverSelector extends TypedEventTarget<ReceiverSelectorEvents> {
|
||||
/** Popup window ID. */
|
||||
private windowId?: number;
|
||||
|
||||
/** Message port to extension page. */
|
||||
private messagePort?: Port;
|
||||
private messagePortDisconnected?: boolean;
|
||||
|
||||
private devices?: ReceiverDevice[];
|
||||
|
||||
private defaultMediaType?: ReceiverSelectorMediaType;
|
||||
private availableMediaTypes?: ReceiverSelectorMediaType;
|
||||
|
||||
private wasReceiverSelected = false;
|
||||
|
||||
appInfo?: ReceiverSelectorAppInfo;
|
||||
pageInfo?: ReceiverSelectorPageInfo;
|
||||
|
||||
constructor(private isBridgeCompatible: boolean) {
|
||||
super();
|
||||
|
||||
this.onConnect = this.onConnect.bind(this);
|
||||
this.onPopupMessage = this.onPopupMessage.bind(this);
|
||||
this.onWindowsRemoved = this.onWindowsRemoved.bind(this);
|
||||
this.onWindowsFocusChanged = this.onWindowsFocusChanged.bind(this);
|
||||
|
||||
browser.windows.onRemoved.addListener(this.onWindowsRemoved);
|
||||
browser.windows.onFocusChanged.addListener(this.onWindowsFocusChanged);
|
||||
|
||||
/**
|
||||
* Handle incoming message channel connection from popup
|
||||
* window script.
|
||||
*/
|
||||
messaging.onConnect.addListener(this.onConnect);
|
||||
}
|
||||
|
||||
/** Is receiver selector window currently open. */
|
||||
get isOpen() {
|
||||
return this.windowId !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and opens a receiver selector window.
|
||||
*/
|
||||
public async open(opts: {
|
||||
devices: ReceiverDevice[];
|
||||
defaultMediaType: ReceiverSelectorMediaType;
|
||||
availableMediaTypes: ReceiverSelectorMediaType;
|
||||
appInfo?: ReceiverSelectorAppInfo;
|
||||
pageInfo?: ReceiverSelectorPageInfo;
|
||||
}) {
|
||||
this.appInfo = opts.appInfo;
|
||||
this.pageInfo = opts.pageInfo;
|
||||
|
||||
// If popup already exists, close it
|
||||
if (this.windowId) {
|
||||
await browser.windows.remove(this.windowId);
|
||||
}
|
||||
|
||||
this.devices = opts.devices;
|
||||
this.defaultMediaType = opts.defaultMediaType;
|
||||
this.availableMediaTypes = opts.availableMediaTypes;
|
||||
|
||||
const popupSizePosition = {
|
||||
width: 400,
|
||||
height: 200,
|
||||
left: 100,
|
||||
top: 100
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current browser window and calculate relative centered
|
||||
* left/top positions for the popup.
|
||||
*/
|
||||
const refWin = await browser.windows.getCurrent();
|
||||
if (refWin.width && refWin.height && refWin.left && refWin.top) {
|
||||
const centerX = refWin.left + refWin.width / 2;
|
||||
const centerY = refWin.top + refWin.height / 3;
|
||||
|
||||
popupSizePosition.left = Math.floor(
|
||||
centerX - popupSizePosition.width / 2
|
||||
);
|
||||
popupSizePosition.top = Math.floor(
|
||||
centerY - popupSizePosition.height / 2
|
||||
);
|
||||
} else {
|
||||
logger.log("Reference window missing positional properties.");
|
||||
}
|
||||
|
||||
// Create popup window
|
||||
const popup = await browser.windows.create({
|
||||
url: POPUP_URL,
|
||||
type: "popup",
|
||||
...popupSizePosition
|
||||
});
|
||||
if (popup?.id === undefined) {
|
||||
throw logger.error("Failed to create receiver selector popup.");
|
||||
}
|
||||
|
||||
// Size/position not set correctly on creation (bug 1396881)
|
||||
await browser.windows.update(popup.id, {
|
||||
...popupSizePosition
|
||||
});
|
||||
|
||||
this.windowId = popup.id;
|
||||
}
|
||||
|
||||
/** Updates receiver devices displayed in the receiver selector. */
|
||||
public update(
|
||||
devices: ReceiverDevice[],
|
||||
isBridgeCompatible: boolean,
|
||||
connectedSessionIds: string[]
|
||||
) {
|
||||
this.devices = devices;
|
||||
this.messagePort?.postMessage({
|
||||
subject: "popup:update",
|
||||
data: { devices, isBridgeCompatible, connectedSessionIds }
|
||||
});
|
||||
}
|
||||
|
||||
/** Closes the receiver selector (if open). */
|
||||
public async close() {
|
||||
if (this.windowId) {
|
||||
await browser.windows.remove(this.windowId);
|
||||
}
|
||||
|
||||
if (this.messagePort && !this.messagePortDisconnected) {
|
||||
this.messagePort.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming port connection from the extension page and
|
||||
* sends init data.
|
||||
*/
|
||||
private onConnect(port: Port) {
|
||||
// Keep history state clean
|
||||
browser.history.deleteUrl({ url: POPUP_URL });
|
||||
|
||||
if (port.name !== "popup") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messagePort?.disconnect();
|
||||
|
||||
this.messagePort = port;
|
||||
this.messagePort.onMessage.addListener(this.onPopupMessage);
|
||||
this.messagePort.onDisconnect.addListener(() => {
|
||||
this.messagePortDisconnected = true;
|
||||
});
|
||||
|
||||
if (
|
||||
this.devices === undefined ||
|
||||
this.defaultMediaType === undefined ||
|
||||
this.availableMediaTypes === undefined
|
||||
) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("error", {
|
||||
detail: "Popup receiver data not found."
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.messagePort.postMessage({
|
||||
subject: "popup:init",
|
||||
data: {
|
||||
appInfo: this.appInfo,
|
||||
pageInfo: this.pageInfo
|
||||
}
|
||||
});
|
||||
|
||||
this.messagePort.postMessage({
|
||||
subject: "popup:update",
|
||||
data: {
|
||||
devices: this.devices,
|
||||
isBridgeCompatible: this.isBridgeCompatible,
|
||||
defaultMediaType: this.defaultMediaType,
|
||||
availableMediaTypes: this.availableMediaTypes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Handles messages from the popup extension page. */
|
||||
private onPopupMessage(message: Message) {
|
||||
switch (message.subject) {
|
||||
case "main:receiverSelected":
|
||||
this.wasReceiverSelected = true;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("selected", { detail: message.data })
|
||||
);
|
||||
break;
|
||||
|
||||
case "main:receiverStopped":
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("stop", { detail: message.data })
|
||||
);
|
||||
break;
|
||||
|
||||
case "main:sendReceiverMessage":
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("receiverMessage", { detail: message.data })
|
||||
);
|
||||
break;
|
||||
case "main:sendMediaMessage":
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("mediaMessage", { detail: message.data })
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cancellation state where the popup window is closed
|
||||
* before a receiver is selected.
|
||||
*/
|
||||
private onWindowsRemoved(windowId: number) {
|
||||
// Only care about popup window
|
||||
if (windowId !== this.windowId) {
|
||||
return;
|
||||
}
|
||||
|
||||
browser.windows.onRemoved.removeListener(this.onWindowsRemoved);
|
||||
browser.windows.onFocusChanged.removeListener(
|
||||
this.onWindowsFocusChanged
|
||||
);
|
||||
|
||||
if (!this.wasReceiverSelected) {
|
||||
this.dispatchEvent(new CustomEvent("cancelled"));
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("close"));
|
||||
|
||||
delete this.windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes popup window if another browser window is brought into
|
||||
* focus. Doesn't apply if no window is focused `WINDOW_ID_NONE`
|
||||
* or if the popup window is re-focused.
|
||||
*/
|
||||
private async onWindowsFocusChanged(windowId: number) {
|
||||
if (!this.windowId) return;
|
||||
|
||||
if (
|
||||
windowId !== browser.windows.WINDOW_ID_NONE &&
|
||||
windowId !== this.windowId
|
||||
) {
|
||||
if (await options.get("receiverSelectorCloseIfFocusLost")) {
|
||||
browser.windows.remove(this.windowId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
extension/src/background/action.ts
Normal file
60
extension/src/background/action.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import logger from "../lib/logger";
|
||||
import castManager from "./castManager";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
const ACTION_ICON_DEFAULT_DARK = "icons/cast-default-dark.svg";
|
||||
const ACTION_ICON_DEFAULT_LIGHT = "icons/cast-default-light.svg";
|
||||
const ACTION_ICON_CONNECTING_DARK = "icons/cast-connecting-dark.svg";
|
||||
const ACTION_ICON_CONNECTING_LIGHT = "icons/cast-connecting-light.svg";
|
||||
const ACTION_ICON_CONNECTED = "icons/cast-connected.svg";
|
||||
|
||||
const isDarkTheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
export enum ActionState {
|
||||
Default,
|
||||
Connecting,
|
||||
Connected
|
||||
}
|
||||
|
||||
/** Updates action details depending on given state. */
|
||||
export function updateActionState(state: ActionState, tabId?: number) {
|
||||
let title: string;
|
||||
let path = isDarkTheme
|
||||
? ACTION_ICON_DEFAULT_LIGHT
|
||||
: ACTION_ICON_DEFAULT_DARK;
|
||||
|
||||
switch (state) {
|
||||
case ActionState.Default:
|
||||
title = _("actionTitleDefault");
|
||||
break;
|
||||
case ActionState.Connecting:
|
||||
title = _("actionTitleConnecting");
|
||||
path = isDarkTheme
|
||||
? ACTION_ICON_CONNECTING_LIGHT
|
||||
: ACTION_ICON_CONNECTING_DARK;
|
||||
break;
|
||||
case ActionState.Connected:
|
||||
title = _("actionTitleConnected");
|
||||
path = ACTION_ICON_CONNECTED;
|
||||
break;
|
||||
}
|
||||
|
||||
browser.browserAction.setTitle({ tabId, title });
|
||||
browser.browserAction.setIcon({ tabId, path });
|
||||
}
|
||||
|
||||
export function initAction() {
|
||||
logger.info("init (action)");
|
||||
|
||||
updateActionState(ActionState.Default);
|
||||
|
||||
browser.browserAction.onClicked.addListener(async tab => {
|
||||
if (tab.id === undefined) {
|
||||
logger.error("Tab ID not found in browser action handler.");
|
||||
return;
|
||||
}
|
||||
|
||||
castManager.triggerCast(tab.id);
|
||||
});
|
||||
}
|
||||
147
extension/src/background/background.ts
Executable file
147
extension/src/background/background.ts
Executable file
@@ -0,0 +1,147 @@
|
||||
import logger from "../lib/logger";
|
||||
import options from "../lib/options";
|
||||
import bridge, { BridgeInfo } from "../lib/bridge";
|
||||
import { baseConfigStorage, fetchBaseConfig } from "../lib/chromecastConfigApi";
|
||||
|
||||
import defaultOptions from "../defaultOptions";
|
||||
import messaging from "../messaging";
|
||||
|
||||
import castManager from "./castManager";
|
||||
import deviceManager from "./deviceManager";
|
||||
|
||||
import { initAction } from "./action";
|
||||
import { initMenus } from "./menus";
|
||||
import { initWhitelist } from "./whitelist";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
/**
|
||||
* On install, set the default options before initializing the
|
||||
* extension. On update, handle any unset values and set to the new
|
||||
* defaults.
|
||||
*/
|
||||
browser.runtime.onInstalled.addListener(async details => {
|
||||
switch (details.reason) {
|
||||
case "install": {
|
||||
// Set defaults
|
||||
await options.setAll(defaultOptions);
|
||||
|
||||
// Extension initialization
|
||||
init();
|
||||
break;
|
||||
}
|
||||
|
||||
case "update": {
|
||||
// Set new defaults
|
||||
await options.update(defaultOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks whether the bridge can be reached and is compatible with the
|
||||
* current version of the extension. If not, triggers a notification
|
||||
* with the appropriate info.
|
||||
*/
|
||||
async function notifyBridgeCompat() {
|
||||
logger.info("checking for bridge...");
|
||||
|
||||
let info: BridgeInfo;
|
||||
try {
|
||||
info = await bridge.getInfo();
|
||||
} catch (err) {
|
||||
logger.info("... bridge issue!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.isVersionCompatible) {
|
||||
logger.info("... bridge compatible!");
|
||||
} else {
|
||||
logger.info("... bridge incompatible!");
|
||||
|
||||
const updateNotificationId = await browser.notifications.create({
|
||||
type: "basic",
|
||||
title: `${_("extensionName")} — ${_(
|
||||
"optionsBridgeIssueStatusTitle"
|
||||
)}`,
|
||||
message: info.isVersionOlder
|
||||
? _("optionsBridgeOlderAction")
|
||||
: _("optionsBridgeNewerAction")
|
||||
});
|
||||
|
||||
browser.notifications.onClicked.addListener(notificationId => {
|
||||
if (notificationId !== updateNotificationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
browser.tabs.create({
|
||||
url: `https://github.com/hensm/fx_cast/releases/tag/v${info.expectedVersion}`
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates locally-stored base config data if never downloaded or since
|
||||
* expired.
|
||||
*/
|
||||
async function cacheBaseConfig() {
|
||||
const { baseConfigUpdated } = await baseConfigStorage.get(
|
||||
"baseConfigUpdated"
|
||||
);
|
||||
|
||||
// If never updated or updated more than 48 hours ago
|
||||
if (
|
||||
!baseConfigUpdated ||
|
||||
(Date.now() - baseConfigUpdated) / 1000 >= 172800
|
||||
) {
|
||||
logger.info("Fetching updated Chromecast base config...");
|
||||
const baseConfig = await fetchBaseConfig();
|
||||
if (baseConfig) {
|
||||
await baseConfigStorage.set({
|
||||
baseConfig,
|
||||
baseConfigUpdated: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
async function init() {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If options haven't been set yet, we can't properly initialize,
|
||||
* so wait until init is called again in the onInstalled listener.
|
||||
*/
|
||||
if (!(await options.getAll())) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("init");
|
||||
isInitialized = true;
|
||||
|
||||
await notifyBridgeCompat();
|
||||
|
||||
await deviceManager.init();
|
||||
await castManager.init();
|
||||
|
||||
await initAction();
|
||||
await initMenus();
|
||||
await initWhitelist();
|
||||
|
||||
messaging.onMessage.addListener(message => {
|
||||
switch (message.subject) {
|
||||
case "main:refreshDeviceManager":
|
||||
deviceManager.refresh();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cacheBaseConfig();
|
||||
init();
|
||||
1122
extension/src/background/castManager.ts
Normal file
1122
extension/src/background/castManager.ts
Normal file
File diff suppressed because it is too large
Load Diff
271
extension/src/background/deviceManager.ts
Normal file
271
extension/src/background/deviceManager.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import bridge, { BridgeInfo } from "../lib/bridge";
|
||||
import logger from "../lib/logger";
|
||||
import { TypedEventTarget } from "../lib/TypedEventTarget";
|
||||
|
||||
import type { Message, Port } from "../messaging";
|
||||
import type { ReceiverDevice } from "../types";
|
||||
|
||||
import type {
|
||||
MediaStatus,
|
||||
ReceiverStatus,
|
||||
SenderMediaMessage,
|
||||
SenderMessage
|
||||
} from "../cast/sdk/types";
|
||||
import { PlayerState } from "../cast/sdk/media/enums";
|
||||
|
||||
import { ActionState, updateActionState } from "./action";
|
||||
|
||||
interface EventMap {
|
||||
deviceUp: { deviceInfo: ReceiverDevice };
|
||||
deviceDown: { deviceId: string };
|
||||
deviceUpdated: { deviceId: string; status: ReceiverStatus };
|
||||
deviceMediaUpdated: { deviceId: string; status: MediaStatus };
|
||||
|
||||
applicationFound: { deviceId: string; appId: string };
|
||||
applicationClosed: { deviceId: string; appId: string; sessionId: string };
|
||||
}
|
||||
|
||||
export default new (class extends TypedEventTarget<EventMap> {
|
||||
/**
|
||||
* Map of receiver device IDs to devices. Updated as receiverDevice
|
||||
* messages are received from the bridge.
|
||||
*/
|
||||
private receiverDevices = new Map<string, ReceiverDevice>();
|
||||
|
||||
private bridgePort?: Port;
|
||||
private bridgeInfo?: BridgeInfo;
|
||||
async init() {
|
||||
if (!this.bridgePort) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes (or re-initializes) a bridge connection to start
|
||||
* dispatching events.
|
||||
*/
|
||||
async refresh() {
|
||||
this.bridgePort?.disconnect();
|
||||
|
||||
try {
|
||||
this.bridgeInfo = await bridge.getInfo();
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
|
||||
if (this.bridgeInfo?.isVersionCompatible) {
|
||||
this.bridgePort = await bridge.connect();
|
||||
this.bridgePort.onMessage.addListener(this.onBridgeMessage);
|
||||
this.bridgePort.onDisconnect.addListener(this.onBridgeDisconnect);
|
||||
|
||||
this.bridgePort.postMessage({
|
||||
subject: "bridge:startDiscovery",
|
||||
data: {
|
||||
// Also send back status messages
|
||||
shouldWatchStatus: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getBridgeInfo() {
|
||||
return this.bridgeInfo;
|
||||
}
|
||||
|
||||
/** Gets a list of receiver devices. */
|
||||
getDevices() {
|
||||
return Array.from(this.receiverDevices.values());
|
||||
}
|
||||
/** Gets a device by ID. */
|
||||
getDeviceById(deviceId: string) {
|
||||
return this.receiverDevices.get(deviceId);
|
||||
}
|
||||
|
||||
/** Sends an NS_RECEIVER message to a given device. */
|
||||
sendReceiverMessage(deviceId: string, message: SenderMessage) {
|
||||
if (!this.bridgePort) {
|
||||
logger.error(
|
||||
"Failed to send receiver message (no bridge connection)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const device = this.receiverDevices.get(deviceId);
|
||||
if (!device) {
|
||||
logger.error(
|
||||
"Failed to send receiver message (could not find device)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.bridgePort?.postMessage({
|
||||
subject: "bridge:sendReceiverMessage",
|
||||
data: { deviceId, message }
|
||||
});
|
||||
}
|
||||
|
||||
/** Sends an NS_MEDIA message to a given device. */
|
||||
sendMediaMessage(deviceId: string, message: SenderMediaMessage) {
|
||||
if (!this.bridgePort) {
|
||||
logger.error("Failed to send media message (no bridge connection)");
|
||||
return;
|
||||
}
|
||||
|
||||
const device = this.receiverDevices.get(deviceId);
|
||||
if (!device) {
|
||||
logger.error(
|
||||
"Failed to send media message (could not find device)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.bridgePort?.postMessage({
|
||||
subject: "bridge:sendMediaMessage",
|
||||
data: { deviceId, message }
|
||||
});
|
||||
}
|
||||
|
||||
private onBridgeMessage = (message: Message) => {
|
||||
switch (message.subject) {
|
||||
case "main:deviceUp": {
|
||||
const { deviceId, deviceInfo } = message.data;
|
||||
|
||||
this.receiverDevices.set(deviceId, deviceInfo);
|
||||
|
||||
// Sort devices by friendly name
|
||||
this.receiverDevices = new Map(
|
||||
[...this.receiverDevices].sort(([, deviceA], [, deviceB]) =>
|
||||
deviceA.friendlyName.localeCompare(deviceB.friendlyName)
|
||||
)
|
||||
);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("deviceUp", {
|
||||
detail: { deviceInfo }
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "main:deviceDown": {
|
||||
const { deviceId } = message.data;
|
||||
|
||||
if (this.receiverDevices.has(deviceId)) {
|
||||
this.receiverDevices.delete(deviceId);
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("deviceDown", {
|
||||
detail: { deviceId: deviceId }
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "main:receiverDeviceStatusUpdated": {
|
||||
const { deviceId, status } = message.data;
|
||||
const device = this.receiverDevices.get(deviceId);
|
||||
if (!device) break;
|
||||
|
||||
const oldApplication = device.status?.applications?.[0];
|
||||
|
||||
// Clear media status when app status changes
|
||||
const application = status.applications?.[0];
|
||||
if (!application || application.isIdleScreen) {
|
||||
delete device.mediaStatus;
|
||||
|
||||
// Send application closed event
|
||||
if (oldApplication && !oldApplication.isIdleScreen) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("applicationClosed", {
|
||||
detail: {
|
||||
deviceId,
|
||||
appId: oldApplication.appId,
|
||||
sessionId: oldApplication.transportId
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
device.status = status;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("deviceUpdated", {
|
||||
detail: {
|
||||
deviceId,
|
||||
status: device.status
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Send new application found event
|
||||
if (
|
||||
!oldApplication &&
|
||||
application &&
|
||||
!application.isIdleScreen
|
||||
) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("applicationFound", {
|
||||
detail: { deviceId, appId: application.appId }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "main:receiverDeviceMediaStatusUpdated": {
|
||||
const { deviceId, status } = message.data;
|
||||
const device = this.receiverDevices.get(deviceId);
|
||||
if (!device) break;
|
||||
|
||||
if (device.mediaStatus) {
|
||||
device.mediaStatus = { ...device.mediaStatus, ...status };
|
||||
if (status.playerState === PlayerState.IDLE) {
|
||||
delete device.mediaStatus.media;
|
||||
}
|
||||
} else {
|
||||
device.mediaStatus = status;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("deviceMediaUpdated", {
|
||||
detail: {
|
||||
deviceId,
|
||||
status: device.mediaStatus
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onBridgeDisconnect = () => {
|
||||
const deviceIds = [...this.receiverDevices.keys()];
|
||||
|
||||
delete this.bridgeInfo;
|
||||
this.receiverDevices.clear();
|
||||
|
||||
// Notify listeners of device availablility
|
||||
for (const deviceId of deviceIds) {
|
||||
const event = new CustomEvent("deviceDown", {
|
||||
detail: { deviceId }
|
||||
});
|
||||
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect 10 seconds after disconnect if not already
|
||||
* reconnected (like immediately after a refresh).
|
||||
*/
|
||||
window.setTimeout(() => {
|
||||
if (!this.bridgeInfo) {
|
||||
this.refresh();
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
})();
|
||||
347
extension/src/background/menus.ts
Normal file
347
extension/src/background/menus.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import logger from "../lib/logger";
|
||||
import options from "../lib/options";
|
||||
import { stringify } from "../lib/utils";
|
||||
|
||||
import * as menuIds from "../menuIds";
|
||||
|
||||
import castManager from "./castManager";
|
||||
|
||||
const _ = browser.i18n.getMessage;
|
||||
|
||||
const URL_PATTERN_HTTP = "http://*/*";
|
||||
const URL_PATTERN_HTTPS = "https://*/*";
|
||||
const URL_PATTERN_FILE = "file://*/*";
|
||||
|
||||
const URL_PATTERNS_REMOTE = [URL_PATTERN_HTTP, URL_PATTERN_HTTPS];
|
||||
const URL_PATTERNS_ALL = [...URL_PATTERNS_REMOTE, URL_PATTERN_FILE];
|
||||
|
||||
type MenuId = string | number;
|
||||
|
||||
let menuIdCast: MenuId;
|
||||
let menuIdCastMedia: MenuId;
|
||||
let menuIdWhitelist: MenuId;
|
||||
let menuIdWhitelistRecommended: MenuId;
|
||||
|
||||
/** Match patterns for the whitelist option menus. */
|
||||
const whitelistChildMenuPatterns = new Map<MenuId, string>();
|
||||
|
||||
/** Handles initial menu setup. */
|
||||
export async function initMenus() {
|
||||
logger.info("init (menus)");
|
||||
|
||||
const opts = await options.getAll();
|
||||
|
||||
// Global "Cast..." menu item
|
||||
menuIdCast = browser.menus.create({
|
||||
contexts: ["browser_action", "page", "tools_menu"],
|
||||
title: _("contextCast"),
|
||||
documentUrlPatterns: ["http://*/*", "https://*/*"],
|
||||
icons: { "16": "icons/icon.svg" } // browser_action context
|
||||
});
|
||||
|
||||
// <video>/<audio> "Cast..." context menu item
|
||||
menuIdCastMedia = browser.menus.create({
|
||||
contexts: ["audio", "video", "image"],
|
||||
title: _("contextCast"),
|
||||
visible: opts.mediaEnabled,
|
||||
targetUrlPatterns: opts.localMediaEnabled
|
||||
? URL_PATTERNS_ALL
|
||||
: URL_PATTERNS_REMOTE
|
||||
});
|
||||
|
||||
menuIdWhitelist = browser.menus.create({
|
||||
contexts: ["browser_action"],
|
||||
title: _("contextAddToWhitelist"),
|
||||
enabled: false
|
||||
});
|
||||
|
||||
menuIdWhitelistRecommended = browser.menus.create({
|
||||
title: _("contextAddToWhitelistRecommended"),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
browser.menus.create({
|
||||
type: "separator",
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
// Popup context menus
|
||||
const createPopupMenu = (props: browser.menus._CreateCreateProperties) =>
|
||||
browser.menus.create({
|
||||
visible: false,
|
||||
documentUrlPatterns: [`${browser.runtime.getURL("ui/popup")}/*`],
|
||||
...props
|
||||
});
|
||||
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_MEDIA_PLAY_PAUSE,
|
||||
title: _("popupMediaPlay")
|
||||
});
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_MEDIA_MUTE,
|
||||
type: "checkbox",
|
||||
title: _("popupMediaMute")
|
||||
});
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_MEDIA_SKIP_PREVIOUS,
|
||||
title: _("popupMediaSkipPrevious")
|
||||
});
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_MEDIA_SKIP_NEXT,
|
||||
title: _("popupMediaSkipNext")
|
||||
});
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_MEDIA_CC,
|
||||
title: _("popupMediaSubtitlesCaptions")
|
||||
});
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_MEDIA_CC_OFF,
|
||||
parentId: menuIds.POPUP_MEDIA_CC,
|
||||
type: "radio",
|
||||
title: _("popupMediaSubtitlesCaptionsOff")
|
||||
});
|
||||
|
||||
createPopupMenu({ id: menuIds.POPUP_MEDIA_SEPARATOR, type: "separator" });
|
||||
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_CAST,
|
||||
title: _("popupCastButtonTitle"),
|
||||
icons: { 16: "icons/icon.svg" }
|
||||
});
|
||||
createPopupMenu({
|
||||
id: menuIds.POPUP_STOP,
|
||||
title: _("popupStopButtonTitle")
|
||||
});
|
||||
|
||||
browser.menus.onShown.addListener(onMenuShown);
|
||||
browser.menus.onClicked.addListener(onMenuClicked);
|
||||
|
||||
options.addEventListener("changed", async ev => {
|
||||
const alteredOpts = ev.detail;
|
||||
const newOpts = await options.getAll();
|
||||
|
||||
if (menuIdCastMedia && alteredOpts.includes("mediaEnabled")) {
|
||||
browser.menus.update(menuIdCastMedia, {
|
||||
visible: newOpts.mediaEnabled
|
||||
});
|
||||
}
|
||||
|
||||
if (menuIdCastMedia && alteredOpts.includes("localMediaEnabled")) {
|
||||
browser.menus.update(menuIdCastMedia, {
|
||||
targetUrlPatterns: newOpts.localMediaEnabled
|
||||
? URL_PATTERNS_ALL
|
||||
: URL_PATTERNS_REMOTE
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Handle updating menus when shown. */
|
||||
async function onMenuShown(info: browser.menus._OnShownInfo) {
|
||||
const menuIds = info.menuIds as unknown as number[];
|
||||
|
||||
// Only rebuild menus if whitelist menu present
|
||||
if (menuIds.includes(menuIdWhitelist as number)) {
|
||||
updateWhitelistMenu(info.pageUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** Handle menu click events */
|
||||
async function onMenuClicked(
|
||||
info: browser.menus.OnClickData,
|
||||
tab?: browser.tabs.Tab
|
||||
) {
|
||||
// Handle whitelist menus
|
||||
if (info.parentMenuItemId === menuIdWhitelist) {
|
||||
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
|
||||
if (!pattern) {
|
||||
throw logger.error(
|
||||
`Whitelist pattern not found for menu item ID ${info.menuItemId}.`
|
||||
);
|
||||
}
|
||||
|
||||
const whitelist = await options.get("siteWhitelist");
|
||||
if (!whitelist.find(item => item.pattern === pattern)) {
|
||||
// Add to whitelist and update options
|
||||
whitelist.push({ pattern, isEnabled: true });
|
||||
await options.set("siteWhitelist", whitelist);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab?.id === undefined) {
|
||||
logger.error("Menu handler tab ID not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info.menuItemId) {
|
||||
case menuIdCast: {
|
||||
castManager.triggerCast(tab.id, info.frameId);
|
||||
break;
|
||||
}
|
||||
|
||||
case menuIdCastMedia:
|
||||
if (info.srcUrl) {
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
code: stringify`
|
||||
window.mediaUrl = ${info.srcUrl};
|
||||
window.targetElementId = ${info.targetElementId};
|
||||
`,
|
||||
frameId: info.frameId
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(tab.id, {
|
||||
file: "cast/senders/media.js",
|
||||
frameId: info.frameId
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles updating the whitelist menus for a given URL */
|
||||
async function updateWhitelistMenu(pageUrl?: string) {
|
||||
/**
|
||||
* If page URL doesn't exist, we're not on a page and have nothing
|
||||
* to whitelist, so disable the menu and return.
|
||||
*/
|
||||
if (!pageUrl) {
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: false
|
||||
});
|
||||
|
||||
browser.menus.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(pageUrl);
|
||||
const urlHasOrigin = url.origin !== "null";
|
||||
|
||||
/**
|
||||
* If the page URL doesn't have an origin, we're not on a
|
||||
* remote page and have nothing to whitelist, so disable the
|
||||
* menu and return.
|
||||
*/
|
||||
if (!urlHasOrigin) {
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: false
|
||||
});
|
||||
|
||||
browser.menus.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable the whitelist menu
|
||||
browser.menus.update(menuIdWhitelist, {
|
||||
enabled: true
|
||||
});
|
||||
|
||||
for (const [menuId] of whitelistChildMenuPatterns) {
|
||||
// Clear all page-specific temporary menus
|
||||
if (menuId !== menuIdWhitelistRecommended) {
|
||||
browser.menus.remove(menuId);
|
||||
}
|
||||
|
||||
whitelistChildMenuPatterns.delete(menuId);
|
||||
}
|
||||
|
||||
// If there is more than one subdomain, get the base domain
|
||||
const baseDomain =
|
||||
(url.hostname.match(/\./g) || []).length > 1
|
||||
? url.hostname.substring(url.hostname.indexOf(".") + 1)
|
||||
: url.hostname;
|
||||
|
||||
const portlessOrigin = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
const patternRecommended = `${portlessOrigin}/*`;
|
||||
const patternSearch = `${portlessOrigin}${url.pathname}${url.search}`;
|
||||
const patternWildcardProtocol = `*://${url.hostname}/*`;
|
||||
const patternWildcardSubdomain = `${url.protocol}//*.${baseDomain}/*`;
|
||||
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
|
||||
|
||||
// Update recommended menu item
|
||||
browser.menus.update(menuIdWhitelistRecommended, {
|
||||
title: _("contextAddToWhitelistRecommended", patternRecommended)
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
menuIdWhitelistRecommended,
|
||||
patternRecommended
|
||||
);
|
||||
|
||||
if (url.search) {
|
||||
const whitelistSearchMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternSearch),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(whitelistSearchMenuId, patternSearch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split URL path into segments and add menu items for each
|
||||
* partial path as the segments are removed.
|
||||
*/
|
||||
{
|
||||
const pathTrimmed = url.pathname.endsWith("/")
|
||||
? url.pathname.substring(0, url.pathname.length - 1)
|
||||
: url.pathname;
|
||||
|
||||
const pathSegments = pathTrimmed
|
||||
.split("/")
|
||||
.filter(segment => segment)
|
||||
.reverse();
|
||||
|
||||
if (pathSegments.length) {
|
||||
for (let i = 0; i < pathSegments.length; i++) {
|
||||
const partialPath = pathSegments.slice(i).reverse().join("/");
|
||||
|
||||
const pattern = `${portlessOrigin}/${partialPath}/*`;
|
||||
|
||||
const partialPathMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", pattern),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(partialPathMenuId, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wildcardProtocolMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardProtocol),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolMenuId,
|
||||
patternWildcardProtocol
|
||||
);
|
||||
|
||||
const wildcardSubdomainMenuId = browser.menus.create({
|
||||
title: _("contextAddToWhitelistAdvancedAdd", patternWildcardSubdomain),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardSubdomainMenuId,
|
||||
patternWildcardSubdomain
|
||||
);
|
||||
|
||||
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
|
||||
title: _(
|
||||
"contextAddToWhitelistAdvancedAdd",
|
||||
patternWildcardProtocolAndSubdomain
|
||||
),
|
||||
parentId: menuIdWhitelist
|
||||
});
|
||||
|
||||
whitelistChildMenuPatterns.set(
|
||||
wildcardProtocolAndSubdomainMenuId,
|
||||
patternWildcardProtocolAndSubdomain
|
||||
);
|
||||
|
||||
await browser.menus.refresh();
|
||||
}
|
||||
268
extension/src/background/whitelist.ts
Normal file
268
extension/src/background/whitelist.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import logger from "../lib/logger";
|
||||
import options from "../lib/options";
|
||||
|
||||
import { getChromeUserAgent } from "../lib/userAgents";
|
||||
import { RemoteMatchPattern } from "../lib/matchPattern";
|
||||
|
||||
import {
|
||||
CAST_FRAMEWORK_LOADER_SCRIPT_URL,
|
||||
CAST_LOADER_SCRIPT_URL
|
||||
} from "../cast/urls";
|
||||
|
||||
// Missing on @types/firefox-webext-browser
|
||||
type OnBeforeSendHeadersDetails = Parameters<
|
||||
Parameters<typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]
|
||||
>[0] & {
|
||||
frameAncestors?: Array<{ url: string; frameId: number }>;
|
||||
};
|
||||
type OnBeforeRequestDetails = Parameters<
|
||||
Parameters<typeof browser.webRequest.onBeforeRequest.addListener>[0]
|
||||
>[0] & {
|
||||
frameAncestors?: Array<{ url: string; frameId: number }>;
|
||||
};
|
||||
|
||||
export interface WhitelistItemData {
|
||||
pattern: string;
|
||||
isEnabled: boolean;
|
||||
isUserAgentDisabled?: boolean;
|
||||
customUserAgent?: string;
|
||||
}
|
||||
|
||||
let matchPatterns: RemoteMatchPattern[] = [];
|
||||
|
||||
let platform: string;
|
||||
let chromeUserAgent: string | undefined;
|
||||
let chromeUserAgentHybrid: string | undefined;
|
||||
|
||||
let siteWhitelistEnabled = false;
|
||||
let siteWhitelist: Nullable<WhitelistItemData[]> = null;
|
||||
let customUserAgent: string | undefined;
|
||||
|
||||
export async function initWhitelist() {
|
||||
logger.info("init (whitelist)");
|
||||
|
||||
if (!platform) {
|
||||
// TODO: Allow hybrid UA to be configurable
|
||||
platform = (await browser.runtime.getPlatformInfo()).os;
|
||||
chromeUserAgent = getChromeUserAgent(platform);
|
||||
chromeUserAgentHybrid = getChromeUserAgent(platform, true);
|
||||
if (!chromeUserAgent) {
|
||||
throw logger.error("Failed to get Chrome UA string");
|
||||
}
|
||||
|
||||
customUserAgent = await options.get("siteWhitelistCustomUserAgent");
|
||||
}
|
||||
|
||||
// Register on first run
|
||||
await registerSiteWhitelist();
|
||||
|
||||
options.addEventListener("changed", async ev => {
|
||||
// Update custom UA on change
|
||||
if (ev.detail.includes("siteWhitelistCustomUserAgent")) {
|
||||
customUserAgent = await options.get("siteWhitelistCustomUserAgent");
|
||||
}
|
||||
// Re-register whitelist on change
|
||||
if (
|
||||
ev.detail.includes("siteWhitelist") ||
|
||||
ev.detail.includes("siteWhitelistEnabled")
|
||||
) {
|
||||
unregisterSiteWhitelist();
|
||||
registerSiteWhitelist();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured user agent matching the specified URL or
|
||||
* undefined if the user agent is disabled.
|
||||
*/
|
||||
function getUserAgent(url: string, host?: string): Optional<string> {
|
||||
if (!siteWhitelistEnabled || !siteWhitelist) return;
|
||||
|
||||
// Search site-specific user agents
|
||||
const matchingItem = siteWhitelist.find(
|
||||
item =>
|
||||
item.customUserAgent &&
|
||||
new RemoteMatchPattern(item.pattern).matches(url)
|
||||
);
|
||||
if (matchingItem) {
|
||||
if (!matchingItem.isEnabled || matchingItem.isUserAgentDisabled) return;
|
||||
return matchingItem.customUserAgent;
|
||||
}
|
||||
|
||||
return (
|
||||
customUserAgent ||
|
||||
(host === "www.youtube.com" ? chromeUserAgentHybrid : chromeUserAgent)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override User-Agent header for requests to whitelisted URLs. Sites
|
||||
* with Chromecast support will usually only load the Cast SDK if they
|
||||
* detect a Chrome user agent string.
|
||||
*/
|
||||
async function onWhitelistedBeforeSendHeaders(
|
||||
details: OnBeforeSendHeadersDetails
|
||||
) {
|
||||
if (!details.requestHeaders) {
|
||||
throw logger.error(
|
||||
"OnBeforeSendHeaders handler details missing requestHeaders."
|
||||
);
|
||||
}
|
||||
|
||||
const host = details.requestHeaders.find(header => header.name === "Host");
|
||||
|
||||
for (const header of details.requestHeaders) {
|
||||
if (header.name === "User-Agent") {
|
||||
header.value = getUserAgent(details.url, host?.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requestHeaders: details.requestHeaders
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Override User-Agent header for requests from child frames of
|
||||
* whitelisted URLs to support embedded players on other origins (e.g.
|
||||
* CDN domains).
|
||||
*/
|
||||
function onWhitelistedChildBeforeSendHeaders(
|
||||
details: OnBeforeSendHeadersDetails
|
||||
) {
|
||||
if (!details.requestHeaders || !details.frameAncestors) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ancestor of details.frameAncestors) {
|
||||
// If no matching patterns
|
||||
if (!matchPatterns.some(pattern => pattern.matches(ancestor.url))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Override User-Agent header
|
||||
const requestHeaders = details.requestHeaders;
|
||||
for (const header of requestHeaders) {
|
||||
if (header.name === "User-Agent") {
|
||||
const host = requestHeaders.find(
|
||||
header => header.name === "Host"
|
||||
);
|
||||
header.value = getUserAgent(details.url, host?.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { requestHeaders };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle requests to cast_sender.js SDK loader script and redirect to
|
||||
* the extension's implementation.
|
||||
*/
|
||||
async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
|
||||
if (!details.originUrl || details.tabId === -1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Test against whitelist if enabled
|
||||
if (await options.get("siteWhitelistEnabled")) {
|
||||
/**
|
||||
* Frame ancestor URLs (if present) or origin URL that the SDK
|
||||
* is loaded from.
|
||||
*/
|
||||
const urls = [
|
||||
...(details.frameAncestors?.map(ancestor => ancestor.url) ?? []),
|
||||
details.originUrl
|
||||
];
|
||||
|
||||
// Allow request if no whitelist matches
|
||||
if (
|
||||
!urls.some(url =>
|
||||
matchPatterns.some(pattern => pattern.matches(url))
|
||||
)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
await browser.tabs.executeScript(details.tabId, {
|
||||
code: `
|
||||
window.isFramework = ${
|
||||
details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL
|
||||
};
|
||||
`,
|
||||
frameId: details.frameId,
|
||||
runAt: "document_start"
|
||||
});
|
||||
|
||||
await browser.tabs.executeScript(details.tabId, {
|
||||
file: "cast/contentBridge.js",
|
||||
frameId: details.frameId,
|
||||
runAt: "document_start"
|
||||
});
|
||||
|
||||
return {
|
||||
redirectUrl: browser.runtime.getURL("cast/content.js")
|
||||
};
|
||||
}
|
||||
|
||||
async function registerSiteWhitelist() {
|
||||
const opts = await options.getAll();
|
||||
siteWhitelist = opts.siteWhitelist;
|
||||
siteWhitelistEnabled = opts.siteWhitelistEnabled;
|
||||
|
||||
// Parse match patterns once
|
||||
matchPatterns = siteWhitelist.map(
|
||||
item => new RemoteMatchPattern(item.pattern)
|
||||
);
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
onBeforeCastSDKRequest,
|
||||
{ urls: [CAST_LOADER_SCRIPT_URL, CAST_FRAMEWORK_LOADER_SCRIPT_URL] },
|
||||
["blocking"]
|
||||
);
|
||||
|
||||
// Skip whitelist request listeners if disabled or empty
|
||||
if (!siteWhitelistEnabled || !siteWhitelist.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
onWhitelistedBeforeSendHeaders,
|
||||
{
|
||||
// Filter for items with UA enabled
|
||||
urls: siteWhitelist.flatMap(item =>
|
||||
item.isEnabled && !item.isUserAgentDisabled
|
||||
? [item.pattern]
|
||||
: []
|
||||
)
|
||||
},
|
||||
["blocking", "requestHeaders"]
|
||||
);
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
onWhitelistedChildBeforeSendHeaders,
|
||||
{ urls: ["<all_urls>"] },
|
||||
["blocking", "requestHeaders"]
|
||||
);
|
||||
|
||||
browser.contentScripts.register({
|
||||
matches: siteWhitelist.map(item => item.pattern),
|
||||
js: [{ file: "cast/contentInitial.js" }],
|
||||
runAt: "document_start",
|
||||
allFrames: true
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSiteWhitelist() {
|
||||
browser.webRequest.onBeforeSendHeaders.removeListener(
|
||||
onWhitelistedBeforeSendHeaders
|
||||
);
|
||||
browser.webRequest.onBeforeSendHeaders.removeListener(
|
||||
onWhitelistedChildBeforeSendHeaders
|
||||
);
|
||||
browser.webRequest.onBeforeRequest.removeListener(onBeforeCastSDKRequest);
|
||||
}
|
||||
Reference in New Issue
Block a user