diff --git a/src/ext/UWContent.ts b/src/ext/UWContent.ts index 766e9ec..6f65401 100644 --- a/src/ext/UWContent.ts +++ b/src/ext/UWContent.ts @@ -7,6 +7,7 @@ import CommsClient from './lib/comms/CommsClient'; import PageInfo from './lib/video-data/PageInfo'; import Logger, { baseLoggingOptions } from './lib/Logger'; import UWGlobals from './lib/UWGlobals'; +import EventBus from './lib/EventBus'; export default class UWContent { pageInfo: PageInfo; @@ -14,6 +15,7 @@ export default class UWContent { settings: Settings; actionHandler: ActionHandler; logger: Logger; + eventBus: EventBus; commsHandlers: { [x: string]: ((a: any, b?: any) => void | Promise)[] @@ -84,16 +86,13 @@ export default class UWContent { console.error("logger init failed!", e) } - // second — let's add some globals - if (! (window as any).ultrawidify) { - (window as any).ultrawidify = new UWGlobals(); - ((window as any).ultrawidify as UWGlobals).importSubscriptionsFromCommsHandlers(this.commsHandlers); - } - // init() is re-run any time settings change if (this.comms) { this.comms.destroy(); } + if (this.eventBus) { + this.eventBus.destroy(); + } if (!this.settings) { this.settings = new Settings({ onSettingsChanged: () => this.reloadSettings(), @@ -102,51 +101,70 @@ export default class UWContent { await this.settings.init(); } - this.comms = new CommsClient('content-main-port', this.logger, this.commsHandlers); - - // če smo razširitev onemogočili v nastavitvah, ne naredimo ničesar - // If extension is soft-disabled, don't do shit - - var extensionMode = this.settings.getExtensionMode(); - - this.logger.log('info', 'debug', "[uw::init] Extension mode:" + (extensionMode < 0 ? "disabled" : extensionMode == '1' ? 'basic' : 'full')); - - const isSiteDisabled = extensionMode === ExtensionMode.Disabled - - if (isSiteDisabled) { - if (this.settings.getExtensionMode('@global') === ExtensionMode.Disabled) { - this.logger.log('info', 'debug', "[uw::init] EXTENSION DISABLED, THEREFORE WONT BE STARTED") - return; + this.eventBus = new EventBus(); + this.eventBus.subscribe( + 'uw-restart', + { + function: () => this.initPhase2() } - } + ); + this.comms = new CommsClient('content-main-port', this.logger, this.eventBus); - try { - if (this.pageInfo) { - this.logger.log('info', 'debug', '[uw.js::setup] An instance of pageInfo already exists and will be destroyed.'); - this.pageInfo.destroy(); - } - this.pageInfo = new PageInfo(this.comms, this.settings, this.logger, extensionMode, isSiteDisabled); - this.logger.log('info', 'debug', "[uw.js::setup] pageInfo initialized."); - - this.logger.log('info', 'debug', "[uw.js::setup] will try to initate ActionHandler."); - - // start action handler only if extension is enabled for this site - if (!isSiteDisabled) { - if (this.actionHandler) { - this.actionHandler.destroy(); - } - this.actionHandler = new ActionHandler(this.pageInfo.eventBus, this.settings, this.logger); - this.actionHandler.init(); - - this.logger.log('info', 'debug', "[uw.js::setup] ActionHandler initiated."); - } - - } catch (e) { - console.error('Ultrawidify: failed to start extension. Error:', e) - this.logger.log('error', 'debug', "[uw::init] FAILED TO START EXTENSION. Error:", e); - } + this.initPhase2(); } catch (e) { console.error('Ultrawidify initalization failed for some reason:', e); } } + + initPhase2() { + // If extension is soft-disabled, don't do shit + var extensionMode = this.settings.getExtensionMode(); + + this.logger.log('info', 'debug', "[uw::init] Extension mode:" + (extensionMode < 0 ? "disabled" : extensionMode == '1' ? 'basic' : 'full')); + + const isSiteDisabled = extensionMode === ExtensionMode.Disabled + + if (isSiteDisabled) { + this.destroy(); + if (this.settings.getExtensionMode('@global') === ExtensionMode.Disabled) { + this.logger.log('info', 'debug', "[uw::init] EXTENSION DISABLED, THEREFORE WONT BE STARTED") + return; + } + } + + try { + if (this.pageInfo) { + this.logger.log('info', 'debug', '[uw.js::setup] An instance of pageInfo already exists and will be destroyed.'); + this.pageInfo.destroy(); + } + this.pageInfo = new PageInfo(this.eventBus, this.settings, this.logger, extensionMode, isSiteDisabled); + this.logger.log('info', 'debug', "[uw.js::setup] pageInfo initialized."); + + this.logger.log('info', 'debug', "[uw.js::setup] will try to initate ActionHandler."); + + // start action handler only if extension is enabled for this site + if (!isSiteDisabled) { + if (this.actionHandler) { + this.actionHandler.destroy(); + } + this.actionHandler = new ActionHandler(this.eventBus, this.settings, this.logger); + this.actionHandler.init(); + + this.logger.log('info', 'debug', "[uw.js::setup] ActionHandler initiated."); + } + + } catch (e) { + console.error('Ultrawidify: failed to start extension. Error:', e) + this.logger.log('error', 'debug', "[uw::init] FAILED TO START EXTENSION. Error:", e); + } + } + + destroy() { + if (this.pageInfo) { + this.pageInfo.destroy(); + } + if (this.actionHandler) { + this.actionHandler.destroy(); + } + } } diff --git a/src/ext/UWServer.ts b/src/ext/UWServer.ts index 23b4dbb..ac524cd 100644 --- a/src/ext/UWServer.ts +++ b/src/ext/UWServer.ts @@ -7,11 +7,13 @@ import Logger, { baseLoggingOptions } from './lib/Logger'; import { sleep } from '../common/js/utils'; import { browser } from 'webextension-polyfill-ts'; +import EventBus, { EventBusCommand } from './lib/EventBus'; export default class UWServer { settings: Settings; logger: Logger; comms: CommsServer; + eventBus: EventBus; ports: any[] = []; hasVideos: boolean; @@ -24,6 +26,26 @@ export default class UWServer { 'videoSettings': undefined, } + eventBusCommands = { + 'popup-set-selected-tab': [{ + function: (message) => this.setSelectedTab(message.selectedMenu, message.selectedSubitem) + }], + 'has-video': [{ + function: (message, context) => this.registerVideo(context.comms.sender) + }], + 'noVideo' : [{ + function: (message, context) => this.unregisterVideo(context.comms.sender) + }], + 'inject-css': [{ + function: (message, context) => this.injectCss(message.cssString, context.comms.sender) + }], + 'eject-css': [{ + function: (message, context) => this.removeCss(message.cssString, context.comms.sender) + }], + 'replace-css': [{ + function: (message, context) => this.replaceCss(message.oldCssString, message.newCssString, context.comms.sender) + }] + }; private gcTimeout: any; uiLoggerInitialized: boolean = false; @@ -52,13 +74,13 @@ export default class UWServer { this.settings = new Settings({logger: this.logger}); await this.settings.init(); + this.eventBus = new EventBus(); this.comms = new CommsServer(this); - this.comms.subscribe('show-logger', async () => await this.initUiAndShowLogger()); - this.comms.subscribe('init-vue', async () => await this.initUi()); - this.comms.subscribe('uwui-vue-initialized', () => this.uiLoggerInitialized = true); + + this.comms.subscribe('emit-logs', () => {}); // we don't need to do anything, this gets forwarded to UI content script as is - browser.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)}); + browser.tabs.onActivated.addListener((m) => {this.onTabSwitched(m)}); } catch (e) { console.error(`Ultrawidify [server]: failed to start. Reason:`, e); } @@ -84,7 +106,7 @@ export default class UWServer { async removeCss(css, sender) { try { browser.tabs.removeCSS(sender.tab.id, {code: css, cssOrigin: 'user', frameId: sender.frameId}); - } catch (e) { + } catch (e) { this.logger.log('error','debug', '[UwServer::injectCss] Error while removing css:', {error: e, css, sender}); } } @@ -98,22 +120,22 @@ export default class UWServer { extractHostname(url){ var hostname; - + if (!url) { return ""; } - // extract hostname + // extract hostname if (url.indexOf("://") > -1) { //find & remove protocol (http, ftp, etc.) and get hostname hostname = url.split('/')[2]; } else { hostname = url.split('/')[0]; } - + hostname = hostname.split(':')[0]; //find & remove port number hostname = hostname.split('?')[0]; //find & remove "?" - + return hostname; } @@ -209,14 +231,14 @@ export default class UWServer { allFrames: true, }); } else if (BrowserDetect.anyChromium) { - await new Promise( resolve => + await new Promise( resolve => chrome.tabs.executeScript({ file: '/ext/uw-ui.js', allFrames: true, }, () => resolve()) ); } - + } catch (e) { console.warn('Ultrawidify [server]: UI setup failed. While problematic, this problem shouldn\'t completely crash the extension.'); this.logger.log('ERROR', 'uwbg', 'UI initialization failed. Reason:', e); @@ -232,16 +254,16 @@ export default class UWServer { await this.initUi(); await new Promise( async (resolve, reject) => { - // if content script doesn't give us a response within 5 seconds, something is + // if content script doesn't give us a response within 5 seconds, something is // obviously wrong and we stop waiting, - // oh and btw, resolve/reject do not break the loops, so we need to do that + // oh and btw, resolve/reject do not break the loops, so we need to do that // ourselves: // https://stackoverflow.com/questions/55207256/will-resolve-in-promise-loop-break-loop-iteration let isRejected = false; setTimeout( async () => {isRejected = true; reject()}, 5000); - // check whether UI has been initiated on the FE. If it was, we resolve the + // check whether UI has been initiated on the FE. If it was, we resolve the // promise and off we go while (!isRejected) { if (this.uiLoggerInitialized) { @@ -261,7 +283,7 @@ export default class UWServer { } async getVideoTab() { - // friendly reminder: if current tab doesn't have a video, + // friendly reminder: if current tab doesn't have a video, // there won't be anything in this.videoTabs[this.currentTabId] const ctab = await this.getCurrentTab(); @@ -295,11 +317,11 @@ export default class UWServer { return { ...this.videoTabs[ctab.id], host: this.extractHostname(ctab.url), - selected: this.selectedSubitem + selected: this.selectedSubitem }; } - // return something more or less empty if this tab doesn't have + // return something more or less empty if this tab doesn't have // a video registered for it return { host: this.extractHostname(ctab.url), @@ -308,7 +330,7 @@ export default class UWServer { } } - // chrome shitiness mitigation + // chrome shitiness mitigation sendUnmarkPlayer(message) { this.comms.sendUnmarkPlayer(message); } diff --git a/src/ext/lib/EventBus.ts b/src/ext/lib/EventBus.ts index 5b049f7..5cbe7f5 100644 --- a/src/ext/lib/EventBus.ts +++ b/src/ext/lib/EventBus.ts @@ -1,7 +1,21 @@ +import CommsClient from './comms/CommsClient'; +import CommsServer from './comms/CommsServer'; export interface EventBusCommand { isGlobal?: boolean, - function: (commandConfig: any) => void | Promise + function: (commandConfig: any, context?: any) => void | Promise +} + +export interface EventBusContext { + stopPropagation?: boolean, + + // Context stuff added by Comms + fromComms?: boolean, + comms?: { + sender?: any, + port?: any, + forwardTo?: 'all' | 'active' | 'contentScript' | 'sameOrigin', + } } export default class EventBus { @@ -9,6 +23,20 @@ export default class EventBus { private commands: { [x: string]: EventBusCommand[]} = {}; private downstreamBuses: EventBus[] = []; private upstreamBus?: EventBus; + private comms?: CommsClient; + + //#region lifecycle + destroy() { + this.commands = null; + for (const bus of this.downstreamBuses) { + bus.destroy(); + } + } + //#endregion + + setComms(comms: CommsClient): void { + this.comms = comms; + } setUpstreamBus(eventBus: EventBus, stopRecursing: boolean = false) { this.upstreamBus = eventBus; @@ -49,18 +77,18 @@ export default class EventBus { } } - send(command: string, config: any, stopPropagation?: boolean) { + send(command: string, config: any, context?: EventBusContext) { if (!this.commands ||!this.commands[command]) { // ensure send is not being called for commands that we have no subscriptions for return; } for (const eventBusCommand of this.commands[command]) { - eventBusCommand.function(config); + eventBusCommand.function(config, context); - if (eventBusCommand.isGlobal && !stopPropagation) { - this.sendUpstream(command, config); - this.sendDownstream(command, config); + if (eventBusCommand.isGlobal && !context?.stopPropagation) { + this.sendUpstream(command, config, context); + this.sendDownstream(command, config, context); } } } @@ -81,14 +109,14 @@ export default class EventBus { } - sendGlobal(command: string, config: any) { + sendGlobal(command: string, config: any, context?: EventBusContext) { this.send(command, config); this.sendUpstream(command, config); this.sendDownstream(command, config); } - sendDownstream(command: string, config: any, sourceEventBus?: EventBus) { + sendDownstream(command: string, config: any, context?: EventBusContext, sourceEventBus?: EventBus) { for (const eventBus of this.downstreamBuses) { if (eventBus !== sourceEventBus) { eventBus.send(command, config); @@ -97,11 +125,14 @@ export default class EventBus { } } - sendUpstream(command: string, config: any) { + sendUpstream(command: string, config: any, context?: EventBusContext) { if (this.upstreamBus) { - this.upstreamBus.send(command, config); - this.upstreamBus.sendUpstream(command, config); - this.upstreamBus.sendDownstream(command, config, this); + this.upstreamBus.send(command, config, context); + this.upstreamBus.sendUpstream(command, config, context); + this.upstreamBus.sendDownstream(command, config, context, this); + } + if (!this.upstreamBus && this.comms && !context?.fromComms) { + this.comms.sendMessage({command, config}); } } } diff --git a/src/ext/lib/comms/CommsClient.ts b/src/ext/lib/comms/CommsClient.ts index 61e36a2..08b49ef 100644 --- a/src/ext/lib/comms/CommsClient.ts +++ b/src/ext/lib/comms/CommsClient.ts @@ -3,124 +3,111 @@ import BrowserDetect from '../../conf/BrowserDetect'; import Logger from '../Logger'; import { browser } from 'webextension-polyfill-ts'; import Settings from '../Settings'; +import EventBus from '../EventBus'; if (process.env.CHANNEL !== 'stable'){ console.info("Loading CommsClient"); } +/** + * Ultrawidify communication spans a few different "domains" that require a few different + * means of communication. The four isolated domains are: + * + * > content script event bus (CS) + * > player UI event bus (UI) + * > UWServer event bus (BG) + * > popup event bus + * + * It is our goal to route messages between various domains. It is our goal that eventBus + * instances in different parts of our script are at least somewhat interoperable between + * each other. As such, scripts sending commands should be unaware that Comms object even + * exists. + * + * EventBus is started first. Other components (including commsClient) follow later. + * + * + * fig 0. ULTRAWIDIFY COMMUNICATION MAP + * + * CS EVENT BUS + * (accessible within tab scripts) + * | NOT EVENT BUS + * PageInfo x (accessible within popup) + * x | + * : : x UWServer + * x CommsClient <---------------x CommsServer x + * | (Connect to popup) + * | + * x eventBus.sendToTunnel() + *